refactor: improve token fetch experience and refactor spinners
All checks were successful
Client CMS Check Build (NixCN CMS) TeamCity build finished
Backend Check Build (NixCN CMS) TeamCity build finished

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-02-13 12:46:12 +08:00
parent fec6fa7312
commit 9f511c0682
7 changed files with 51 additions and 28 deletions

View File

@@ -1,7 +1,7 @@
import type { EventInfo } from './types';
import { Loader2 } from 'lucide-react';
import { Button } from '../ui/button';
import { DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { Spinner } from '../ui/spinner';
export function EventJoinDialogView({ event, onJoinEvent, isPending }: { event: EventInfo; onJoinEvent: () => void; isPending: boolean }) {
return (
@@ -19,7 +19,7 @@ export function EventJoinDialogView({ event, onJoinEvent, isPending }: { event:
<DialogClose asChild>
<Button variant="outline"></Button>
</DialogClose>
<Button onClick={onJoinEvent} disabled={isPending}>{isPending ? <Loader2 className="animate-spin" /> : '加入'}</Button>
<Button onClick={onJoinEvent} disabled={isPending}>{isPending ? <Spinner /> : '加入'}</Button>
</DialogFooter>
</DialogContent>
);

View File

@@ -1,6 +1,5 @@
import type { KycSubmission } from './kyc.types';
import { useForm } from '@tanstack/react-form';
import { Loader2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import z from 'zod';
@@ -12,6 +11,7 @@ import {
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Spinner } from '@/components/ui/spinner';
import { Button } from '../../ui/button';
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
@@ -81,7 +81,7 @@ function CnridForm({ onSubmit }: { onSubmit: OnSubmit }) {
<form.Subscribe
selector={state => [state.canSubmit, state.isPristine, state.isSubmitting]}
children={([canSubmit, isPristine, isSubmitting]) => (
<Button type="submit" disabled={!canSubmit || isPristine || isSubmitting}>{isSubmitting ? <Loader2 className="animate-spin" /> : '开始认证'}</Button>
<Button type="submit" disabled={!canSubmit || isPristine || isSubmitting}>{isSubmitting ? <Spinner /> : '开始认证'}</Button>
)}
/>
</DialogFooter>

View File

@@ -3,7 +3,6 @@ import type { AuthorizeSearchParams } from '@/routes/authorize';
import { Turnstile } from '@marsidev/react-turnstile';
import { useForm } from '@tanstack/react-form';
import { useNavigate } from '@tanstack/react-router';
import { Loader2 } from 'lucide-react';
import { useRef, useState } from 'react';
import { toast } from 'sonner';
import z from 'zod';
@@ -18,6 +17,7 @@ import {
import { Input } from '@/components/ui/input';
import { useGetMagicLink } from '@/hooks/data/useGetMagicLink';
import { cn } from '@/lib/utils';
import { Spinner } from './ui/spinner';
export function LoginForm({
oauthParams,
@@ -91,7 +91,7 @@ export function LoginForm({
</form.Field>
<Field>
<Button type="submit" disabled={isLoading}>
{isLoading && <Loader2 className="animate-spin" />}
{isLoading && <Spinner />}
{token === null ? '等待 Turnstile' : isPending ? '发送中...' : '发送登录链接'}
</Button>
</Field>

View File

@@ -1,6 +1,5 @@
import type { ServiceUserUserInfoData } from '@/client';
import { useForm } from '@tanstack/react-form';
import { Loader2 } from 'lucide-react';
import {
useEffect,
useState,
@@ -25,6 +24,7 @@ import {
import {
Input,
} from '@/components/ui/input';
import { Spinner } from '../ui/spinner';
import { Switch } from '../ui/switch';
const formSchema = z.object({
@@ -168,7 +168,7 @@ export function EditProfileDialogView({ user, updateProfile }: { user: ServiceUs
selector={state => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<Button type="submit" disabled={!canSubmit}>
{isSubmitting ? <Loader2 className="animate-spin" /> : '保存'}
{isSubmitting ? <Spinner /> : '保存'}
</Button>
)}
/>

View File

@@ -6,13 +6,14 @@ import {
isEmpty,
isNil,
} from 'lodash-es';
import { Loader2, Mail, Pencil } from 'lucide-react';
import { Mail, Pencil } from 'lucide-react';
import { useMemo, useState } from 'react';
import Markdown from 'react-markdown';
import { toast } from 'sonner';
import { Avatar, AvatarImage } from '@/components/ui/avatar';
import { base64ToUtf8 } from '@/lib/utils';
import { Button } from '../ui/button';
import { Spinner } from '../ui/spinner';
import { EditProfileDialogContainer } from './edit-profile.dialog.container';
export function ProfileView({ user, onSaveBio }: { user: ServiceUserUserInfoData; onSaveBio: (bio: string) => Promise<void> }) {
@@ -89,7 +90,7 @@ export function ProfileView({ user, onSaveBio }: { user: ServiceUserUserInfoData
variant={enableBioEdit ? 'default' : 'outline'}
disabled={isSubmittingBio}
>
{isSubmittingBio ? <Loader2 className="animate-spin" /> : <Pencil />}
{isSubmittingBio ? <Spinner /> : <Pencil />}
</Button>
</section>
</div>

View File

@@ -0,0 +1,16 @@
import { Loader2Icon } from "lucide-react"
import { cn } from "@/lib/utils"
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (
<Loader2Icon
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin", className)}
{...props}
/>
)
}
export { Spinner }

View File

@@ -2,10 +2,10 @@ import { useMutation } from '@tanstack/react-query';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import {
useEffect,
useState,
} from 'react';
import z from 'zod';
import { postAuthTokenMutation } from '@/client/@tanstack/react-query.gen';
import { Spinner } from '@/components/ui/spinner';
import { setAccessToken, setRefreshToken } from '@/lib/token';
const tokenCodeSchema = z.object({
@@ -19,27 +19,33 @@ export const Route = createFileRoute('/token')({
function RouteComponent() {
const { code } = Route.useSearch();
const [status, setStatus] = useState('Loading...');
const navigate = useNavigate();
const mutation = useMutation({
const { mutate } = useMutation({
...postAuthTokenMutation(),
onSuccess: (data) => {
setAccessToken(data.data!.access_token!);
setRefreshToken(data.data!.refresh_token!);
void navigate({ to: '/' });
},
onError: () => {
setStatus('Error getting token');
},
throwOnError: true,
});
useEffect(() => {
if (mutation.isIdle) {
mutation.mutate({ body: { code } });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const controller = new AbortController();
mutate({
body: { code },
signal: controller.signal,
});
return <div>{status}</div>;
return () => {
controller.abort();
};
}, [code, mutate]);
return (
<div className="flex min-h-screen items-center justify-center">
<Spinner className="size-8" />
</div>
);
}