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

View File

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

View File

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

View File

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

View File

@@ -6,13 +6,14 @@ import {
isEmpty, isEmpty,
isNil, isNil,
} from 'lodash-es'; } from 'lodash-es';
import { Loader2, Mail, Pencil } from 'lucide-react'; import { Mail, Pencil } from 'lucide-react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import Markdown from 'react-markdown'; import Markdown from 'react-markdown';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Avatar, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarImage } from '@/components/ui/avatar';
import { base64ToUtf8 } from '@/lib/utils'; import { base64ToUtf8 } from '@/lib/utils';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Spinner } from '../ui/spinner';
import { EditProfileDialogContainer } from './edit-profile.dialog.container'; import { EditProfileDialogContainer } from './edit-profile.dialog.container';
export function ProfileView({ user, onSaveBio }: { user: ServiceUserUserInfoData; onSaveBio: (bio: string) => Promise<void> }) { 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'} variant={enableBioEdit ? 'default' : 'outline'}
disabled={isSubmittingBio} disabled={isSubmittingBio}
> >
{isSubmittingBio ? <Loader2 className="animate-spin" /> : <Pencil />} {isSubmittingBio ? <Spinner /> : <Pencil />}
</Button> </Button>
</section> </section>
</div> </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 { createFileRoute, useNavigate } from '@tanstack/react-router';
import { import {
useEffect, useEffect,
useState,
} from 'react'; } from 'react';
import z from 'zod'; import z from 'zod';
import { postAuthTokenMutation } from '@/client/@tanstack/react-query.gen'; import { postAuthTokenMutation } from '@/client/@tanstack/react-query.gen';
import { Spinner } from '@/components/ui/spinner';
import { setAccessToken, setRefreshToken } from '@/lib/token'; import { setAccessToken, setRefreshToken } from '@/lib/token';
const tokenCodeSchema = z.object({ const tokenCodeSchema = z.object({
@@ -19,27 +19,33 @@ export const Route = createFileRoute('/token')({
function RouteComponent() { function RouteComponent() {
const { code } = Route.useSearch(); const { code } = Route.useSearch();
const [status, setStatus] = useState('Loading...');
const navigate = useNavigate(); const navigate = useNavigate();
const mutation = useMutation({ const { mutate } = useMutation({
...postAuthTokenMutation(), ...postAuthTokenMutation(),
onSuccess: (data) => { onSuccess: (data) => {
setAccessToken(data.data!.access_token!); setAccessToken(data.data!.access_token!);
setRefreshToken(data.data!.refresh_token!); setRefreshToken(data.data!.refresh_token!);
void navigate({ to: '/' }); void navigate({ to: '/' });
}, },
onError: () => { throwOnError: true,
setStatus('Error getting token');
},
}); });
useEffect(() => { useEffect(() => {
if (mutation.isIdle) { const controller = new AbortController();
mutation.mutate({ body: { code } }); mutate({
} body: { code },
// eslint-disable-next-line react-hooks/exhaustive-deps 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>
);
} }