refactor: improve token fetch experience and refactor spinners
Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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> }) {
|
||||||
@@ -54,12 +55,12 @@ export function ProfileView({ user, onSaveBio }: { user: ServiceUserUserInfoData
|
|||||||
{/* Bio */}
|
{/* Bio */}
|
||||||
{enableBioEdit
|
{enableBioEdit
|
||||||
? (
|
? (
|
||||||
<MDEditor
|
<MDEditor
|
||||||
value={bio}
|
value={bio}
|
||||||
onChange={setBio}
|
onChange={setBio}
|
||||||
height="100%"
|
height="100%"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: <div className="p-6 prose dark:prose-invert"><Markdown>{bio}</Markdown></div>}
|
: <div className="p-6 prose dark:prose-invert"><Markdown>{bio}</Markdown></div>}
|
||||||
<Button
|
<Button
|
||||||
className="absolute bottom-4 right-4"
|
className="absolute bottom-4 right-4"
|
||||||
@@ -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>
|
||||||
|
|||||||
16
client/cms/src/components/ui/spinner.tsx
Normal file
16
client/cms/src/components/ui/spinner.tsx
Normal 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 }
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user