mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-30 14:30:08 +08:00
spinners! loading buttons!
also improved new project creation finishing page
This commit is contained in:
parent
7af707538f
commit
2fead3423f
@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import Spinner from "~/components/design/Spinner.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "click"): void;
|
||||
@ -9,11 +10,13 @@ const props = withDefaults(
|
||||
disabled?: boolean;
|
||||
size?: "small" | "medium" | "large";
|
||||
buttonType?: "primary" | "gray" | "red" | "transparent";
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
size: "small",
|
||||
buttonType: "primary",
|
||||
loading: false,
|
||||
}
|
||||
);
|
||||
const paddingClass = computed(() => {
|
||||
@ -34,11 +37,12 @@ const paddingClass = computed(() => {
|
||||
|
||||
<template>
|
||||
<button
|
||||
:class="'rounded-md font-semibold h-min inline-flex items-center ' + paddingClass + ' button-' + buttonType"
|
||||
:disabled="disabled"
|
||||
:class="'rounded-md font-semibold h-min inline-flex items-center ' + paddingClass + ' button-' + buttonType + (loading ? ' !cursor-wait' : '')"
|
||||
:disabled="disabled || loading"
|
||||
v-bind="$attrs"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<slot></slot>
|
||||
<span v-if="loading" class="pl-1"><Spinner class="stroke-gray-400" :diameter="1" :stroke="0.01" unit="rem" /></span>
|
||||
</button>
|
||||
</template>
|
||||
|
239
frontend-new/src/components/design/Spinner.vue
Normal file
239
frontend-new/src/components/design/Spinner.vue
Normal file
@ -0,0 +1,239 @@
|
||||
<!--
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2020 Marcos Moura, Creative Tim (https://www.creative-tim.com) & Community
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
-->
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value?: number;
|
||||
diameter?: number;
|
||||
stroke?: number;
|
||||
indeterminate?: boolean;
|
||||
unit?: string;
|
||||
}>(),
|
||||
{
|
||||
value: 0,
|
||||
diameter: 24,
|
||||
stroke: 2,
|
||||
indeterminate: true,
|
||||
unit: "px",
|
||||
}
|
||||
);
|
||||
|
||||
const svg = ref();
|
||||
const circle = ref();
|
||||
|
||||
const circleRadius = computed(() => (props.diameter - props.stroke) / 2);
|
||||
const circleCircumference = computed(() => 2 * Math.PI * circleRadius.value);
|
||||
|
||||
watch(
|
||||
props,
|
||||
() => {
|
||||
attachSvgStyle();
|
||||
attachCircleStyle();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
attachSvgStyle();
|
||||
attachCircleStyle();
|
||||
});
|
||||
|
||||
function attachSvgStyle() {
|
||||
const size = props.diameter + props.unit;
|
||||
svg.value.style.width = size;
|
||||
svg.value.style.height = size;
|
||||
}
|
||||
|
||||
function attachCircleStyle() {
|
||||
if (!props.indeterminate) {
|
||||
circle.value.style.strokeDashoffset = (circleCircumference.value * (100 - props.value)) / 100 + props.unit;
|
||||
}
|
||||
circle.value.style.strokeDasharray = circleCircumference.value;
|
||||
circle.value.style.strokeWidth = props.stroke + props.unit;
|
||||
circle.value.style.setProperty("--md-progress-spinner-start-value", String(0.95 * circleCircumference.value));
|
||||
circle.value.style.setProperty("--md-progress-spinner-end-value", String(0.2 * circleCircumference.value));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="md-progress-spinner" appear>
|
||||
<div
|
||||
class="md-progress-spinner"
|
||||
:class="{ 'md-progress-spinner-indeterminate': true, 'md-determinate': !indeterminate, 'md-indeterminate': indeterminate }"
|
||||
>
|
||||
<svg ref="svg" class="md-progress-spinner-draw" preserveAspectRatio="xMidYMid meet" focusable="false" :viewBox="`0 0 ${diameter} ${diameter}`">
|
||||
<circle ref="circle" class="md-progress-spinner-circle" cx="50%" cy="50%" :r="circleRadius"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
$md-transition-stand-timing: cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
$md-transition-stand-duration: 0.4s;
|
||||
$md-transition-stand: $md-transition-stand-duration $md-transition-stand-timing;
|
||||
|
||||
@keyframes md-progress-spinner-rotate {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes md-progress-spinner-initial-rotate {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: rotate(-90deg) translateZ(0);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(270deg) translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes md-progress-spinner-stroke-rotate {
|
||||
0% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-start-value);
|
||||
transform: rotate(0);
|
||||
}
|
||||
12.5% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-end-value);
|
||||
transform: rotate(0);
|
||||
}
|
||||
12.51% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-end-value);
|
||||
transform: rotateX(180deg) rotate(72.5deg);
|
||||
}
|
||||
25% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-start-value);
|
||||
transform: rotateX(180deg) rotate(72.5deg);
|
||||
}
|
||||
25.1% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-start-value);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
37.5% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-end-value);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
37.51% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-end-value);
|
||||
transform: rotateX(180deg) rotate(161.5deg);
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-start-value);
|
||||
transform: rotateX(180deg) rotate(161.5deg);
|
||||
}
|
||||
50.01% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-start-value);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
62.5% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-end-value);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
62.51% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-end-value);
|
||||
transform: rotateX(180deg) rotate(251.5deg);
|
||||
}
|
||||
75% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-start-value);
|
||||
transform: rotateX(180deg) rotate(251.5deg);
|
||||
}
|
||||
75.01% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-start-value);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
87.5% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-end-value);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
87.51% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-end-value);
|
||||
transform: rotateX(180deg) rotate(341.5deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: var(--md-progress-spinner-start-value);
|
||||
transform: rotateX(180deg) rotate(341.5deg);
|
||||
}
|
||||
}
|
||||
.md-progress-spinner {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
&.md-indeterminate {
|
||||
animation: md-progress-spinner-rotate 2s linear infinite;
|
||||
&.md-progress-spinner-enter,
|
||||
&.md-progress-spinner-leave-to {
|
||||
.md-progress-spinner-draw {
|
||||
opacity: 0;
|
||||
transform: scale(0.1);
|
||||
}
|
||||
}
|
||||
&.md-progress-spinner-enter-active,
|
||||
&.md-progress-spinner-leave-active {
|
||||
transition-duration: 0.4s;
|
||||
animation: none;
|
||||
}
|
||||
.md-progress-spinner-circle {
|
||||
animation: 4s infinite $md-transition-stand-timing;
|
||||
animation-name: md-progress-spinner-stroke-rotate;
|
||||
}
|
||||
}
|
||||
&.md-determinate {
|
||||
&.md-progress-spinner-enter-active {
|
||||
transition-duration: 2s;
|
||||
.md-progress-spinner-draw {
|
||||
animation: md-progress-spinner-initial-rotate 1.98s $md-transition-stand-timing forwards;
|
||||
}
|
||||
}
|
||||
&.md-progress-spinner-leave-active {
|
||||
transition-duration: 2s;
|
||||
.md-progress-spinner-draw {
|
||||
animation: md-progress-spinner-initial-rotate reverse 1.98s $md-transition-stand-timing forwards;
|
||||
}
|
||||
}
|
||||
.md-progress-spinner-draw {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.md-progress-spinner-draw {
|
||||
overflow: visible;
|
||||
transform: scale(1) rotate(-90deg);
|
||||
transform-origin: center;
|
||||
transition: 0.4s $md-transition-stand-timing;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
.md-progress-spinner-circle {
|
||||
fill: none;
|
||||
transform-origin: center;
|
||||
transition: stroke-dashoffset 0.25s $md-transition-stand-timing;
|
||||
will-change: stroke-dashoffset, stroke-dasharray, stroke-width, animation-name, r;
|
||||
}
|
||||
</style>
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
|
||||
import Spinner from "~/components/design/Spinner.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
errors?: string[];
|
||||
@ -27,8 +28,7 @@ const props = defineProps<{
|
||||
<span class="flex pl-2">
|
||||
<span v-if="counter && maxlength" class="inline-flex items-center ml-2">{{ value?.length || 0 }}/{{ maxlength }}</span>
|
||||
<span v-else-if="counter">{{ value?.length || 0 }}</span>
|
||||
<!-- todo proper loading indicator -->
|
||||
<span v-if="loading">Loading...</span>
|
||||
<span v-if="loading" class="w-[24px] h-[24px]"><Spinner class="stroke-gray-400" /></span>
|
||||
</span>
|
||||
<span
|
||||
v-if="label"
|
||||
|
@ -26,7 +26,7 @@ export function handleRequestError(
|
||||
} else if (err.response.data.isHangarValidationException) {
|
||||
const data: HangarValidationException = err.response.data;
|
||||
for (const fieldError of data.fieldErrors) {
|
||||
notfication.error(fieldError.errorMsg);
|
||||
notfication.error(i18n.te(fieldError.errorMsg) ? i18n.t(fieldError.errorMsg) : fieldError.errorMsg);
|
||||
}
|
||||
if (msg) {
|
||||
notfication.error(i18n.t(msg));
|
||||
|
@ -4,14 +4,12 @@ import * as validators from "@vuelidate/validators";
|
||||
import { createI18nMessage, helpers, ValidatorWrapper } from "@vuelidate/validators";
|
||||
import { I18n } from "~/i18n";
|
||||
import { useInternalApi } from "~/composables/useApi";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
export function isErrorObject(errorObject: string | ErrorObject): errorObject is ErrorObject {
|
||||
return (<ErrorObject>errorObject).$message !== undefined;
|
||||
}
|
||||
|
||||
export function constructValidators<T>(rules: ValidationRule<T | undefined>[] | undefined, name: string) {
|
||||
// no clue why this cast is needed
|
||||
return rules ? { [name]: rules } : { [name]: {} };
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import Markdown from "~/components/Markdown.vue";
|
||||
import InputTextarea from "~/components/ui/InputTextarea.vue";
|
||||
import { useVuelidate } from "@vuelidate/core";
|
||||
import { required, maxLength, validName, pattern, url, requiredIf } from "~/composables/useValidationHelpers";
|
||||
import Spinner from "~/components/design/Spinner.vue";
|
||||
|
||||
interface NewProjectForm extends ProjectSettingsForm {
|
||||
ownerId: ProjectOwner["userId"];
|
||||
@ -101,7 +102,11 @@ function convertBBCode() {
|
||||
|
||||
function createProject() {
|
||||
projectCreationErrors.value = [];
|
||||
useInternalApi<string>("projects/create", true, "post", form)
|
||||
projectLoading.value = true;
|
||||
if (!form.value.pageContent) {
|
||||
form.value.pageContent = "# " + form.value.name + " \nWelcome to your new project!";
|
||||
}
|
||||
useInternalApi<string>("projects/create", true, "post", form.value)
|
||||
.then((url) => {
|
||||
router.push(url);
|
||||
})
|
||||
@ -114,19 +119,6 @@ function createProject() {
|
||||
}
|
||||
|
||||
handleRequestError(err, ctx, i18n, "project.new.error.create");
|
||||
});
|
||||
}
|
||||
|
||||
function retry() {
|
||||
if (!form.value.pageContent) {
|
||||
form.value.pageContent = "# " + form.value.name + " \nWelcome to your new project!";
|
||||
}
|
||||
useInternalApi<string>("projects/create", true, "post", form.value)
|
||||
.then((url) => {
|
||||
router.push(url);
|
||||
})
|
||||
.catch((err) => {
|
||||
projectCreationErrors.value.push(err.response.data);
|
||||
})
|
||||
.finally(() => {
|
||||
projectLoading.value = false;
|
||||
@ -269,21 +261,19 @@ function retry() {
|
||||
</Tabs>
|
||||
</template>
|
||||
<template #finishing>
|
||||
<!-- todo loader -->
|
||||
<!--<v-progress-circular v-if="projectLoading" indeterminate color="red" size="50" />-->
|
||||
<span v-if="projectLoading">
|
||||
Loading....
|
||||
<Button @click="retry">button go brrrr</Button>
|
||||
</span>
|
||||
<template v-else-if="projectCreationErrors && projectCreationErrors.length > 0">
|
||||
<div class="text-lg mt-2">
|
||||
{{ i18n.t("project.new.error.create") }}
|
||||
{{ projectCreationErrors }}
|
||||
<div class="flex flex-col">
|
||||
<div v-if="projectLoading" class="text-center my-8"><Spinner class="stroke-red-500" :diameter="90" :stroke="6" /></div>
|
||||
<div v-if="projectLoading" class="text-center"><Button @click="createProject">button go brrrr</Button></div>
|
||||
<template v-else-if="projectCreationErrors && projectCreationErrors.length > 0">
|
||||
<div class="text-lg mt-2">
|
||||
{{ i18n.t("project.new.error.create") }}
|
||||
{{ projectCreationErrors }}
|
||||
</div>
|
||||
<div class="text-center mt-2"><Button @click="createProject"> Retry </Button></div>
|
||||
</template>
|
||||
<div v-else class="text-h5 mt-2">
|
||||
{{ i18n.t("project.new.step5.text") }}
|
||||
</div>
|
||||
<Button @click="retry"> Retry </Button>
|
||||
</template>
|
||||
<div v-else class="text-h5 mt-2">
|
||||
{{ i18n.t("project.new.step5.text") }}
|
||||
</div>
|
||||
</template>
|
||||
</Steps>
|
||||
|
Loading…
Reference in New Issue
Block a user