Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2024-10-11 16:17:09 +08:00
parent eb2613114b
commit b69c6f97c8
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
8 changed files with 205 additions and 20 deletions

View File

@ -4,9 +4,12 @@ import { getCsrfToken } from "@hono/auth-js/react"
import PKG from "@pkg"
import { hc } from "hono/client"
import { FetchError, ofetch } from "ofetch"
import { createElement } from "react"
import { toast } from "sonner"
import { NetworkStatus, setApiStatus } from "~/atoms/network"
import { setLoginModalShow } from "~/atoms/user"
import { NeedActivationToast } from "~/modules/activation/NeedActivationToast"
let csrfTokenPromise: Promise<string> | null = null
export const apiFetch = ofetch.create({
@ -63,6 +66,20 @@ export const apiFetch = ofetch.create({
if (context.response.status === 400 && json.code === 1003) {
router.navigate("/invitation")
}
if (json.code.toString().startsWith("11")) {
setTimeout(() => {
const toastId = toast.error(
createElement(NeedActivationToast, {
dimiss: () => {
toast.dismiss(toastId)
},
}),
{
duration: 10e4,
},
)
}, 500)
}
} catch {
// ignore
}

View File

@ -0,0 +1,113 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { DotLottieReact } from "@lottiefiles/dotlottie-react"
import { m } from "framer-motion"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { Button } from "~/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/components/ui/form"
import { Input } from "~/components/ui/input"
import { useCurrentModal } from "~/components/ui/modal"
import { getFetchErrorMessage } from "~/lib/error-parser"
import confettiUrl from "~/lottie/confetti.lottie?url"
import { auth } from "~/queries/auth"
import { useInvitationMutation } from "~/queries/invitations"
const formSchema = z.object({
code: z.string().min(1),
})
const absoluteConfettiUrl = new URL(confettiUrl, import.meta.url).href
export const ActivationModalContent = () => {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
})
const { setClickOutSideToDismiss } = useCurrentModal()
const invitationMutation = useInvitationMutation({
onError(error) {
const message = getFetchErrorMessage(error)
form.setError("code", { message })
},
})
const { t } = useTranslation()
function onSubmit(values: z.infer<typeof formSchema>) {
invitationMutation.mutate(values.code)
}
const [showConfetti, setShowConfetti] = useState(false)
useEffect(() => {
if (invitationMutation.isSuccess) {
setShowConfetti(true)
setClickOutSideToDismiss(true)
auth.getSession().invalidate()
}
}, [invitationMutation.isSuccess, setClickOutSideToDismiss])
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="w-[512px] max-w-full overflow-hidden px-0.5"
>
<FormField
control={form.control}
name="code"
render={({ field }) => (
<FormItem className="flex flex-col items-center gap-2 md:block">
<FormLabel className="!text-foreground">{t("activation.description")}</FormLabel>
<FormControl>
<Input
autoFocus
className="font-mono placeholder:font-default"
placeholder={t("activation.title")}
{...field}
/>
</FormControl>
<div className="h-6">
<FormMessage />
</div>
</FormItem>
)}
/>
<div className="center relative flex">
{showConfetti && (
<DotLottieReact
className="absolute z-[1] size-[120px]"
src={absoluteConfettiUrl}
loop={false}
autoplay
/>
)}
<Button
variant={showConfetti ? "outline" : "primary"}
type="submit"
disabled={!form.formState.isValid}
isLoading={invitationMutation.isPending}
>
{showConfetti && (
<m.i
initial={{ y: 5 }}
animate={{
y: 0,
}}
className="i-mgc-check-circle-filled mr-2 text-green-500"
/>
)}
{t("activation.activate")}
</Button>
</div>
</form>
</Form>
)
}

View File

@ -0,0 +1,32 @@
import { useCallback } from "react"
import { useTranslation } from "react-i18next"
import { Button } from "~/components/ui/button"
import { stopPropagation } from "~/lib/dom"
import { useActivationModal } from "."
export const NeedActivationToast = (props: { dimiss: () => void }) => {
const presentActivationModal = useActivationModal()
const { t } = useTranslation()
return (
<div className="flex flex-col justify-end">
<div>{t("activation.description")}</div>
<div className="text-right">
<Button
// Prevent modal dismiss
onPointerDown={stopPropagation}
onClick={useCallback(() => {
presentActivationModal()
props.dimiss()
}, [presentActivationModal, props])}
buttonClassName="text-xs"
>
{t("activation.activate")}
</Button>
</div>
</div>
)
}

View File

@ -0,0 +1,20 @@
import { useCallback } from "react"
import { useTranslation } from "react-i18next"
import { useModalStack } from "~/components/ui/modal"
import { ActivationModalContent } from "./ActivationModalContent"
export const useActivationModal = () => {
const { present } = useModalStack()
const { t } = useTranslation()
return useCallback(
() =>
present({
title: t("activation.title"),
content: ActivationModalContent,
id: "activation",
}),
[present, t],
)
}

View File

@ -1,18 +1,22 @@
import { env } from "@follow/shared/env"
import { useUserRole } from "~/atoms/user"
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "~/components/ui/tooltip"
export const EnvironmentIndicator = () => (
<Tooltip>
<TooltipTrigger asChild>
<div className="fixed bottom-0 right-0 z-[99999] rounded-tl bg-accent px-1 py-0.5 text-xs text-white">
{import.meta.env.MODE}
</div>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent className="max-w-max break-all" side="top">
<pre>{JSON.stringify({ ...env }, null, 2)}</pre>
</TooltipContent>
</TooltipPortal>
</Tooltip>
)
export const EnvironmentIndicator = () => {
const role = useUserRole()
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="fixed bottom-0 right-0 z-[99999] rounded-tl bg-accent px-1 py-0.5 text-xs text-white">
{role}:{import.meta.env.MODE}
</div>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent className="max-w-max break-all" side="top">
<pre>{JSON.stringify({ ...env }, null, 2)}</pre>
</TooltipContent>
</TooltipPortal>
</Tooltip>
)
}

View File

@ -10,9 +10,7 @@ export const loader = defineSettingPageData({
iconName,
name: "titles.invitations",
priority,
hideIf(ctx) {
return ctx.role !== UserRole.Trial
},
hideIf: (ctx) => ctx.role === UserRole.Trial,
})
export function Component() {

View File

@ -10,9 +10,7 @@ export const loader = defineSettingPageData({
iconName,
name: "titles.lists",
priority,
hideIf(ctx) {
return ctx.role !== UserRole.Trial
},
hideIf: (ctx) => ctx.role === UserRole.Trial,
})
export function Component() {

View File

@ -3,6 +3,9 @@
"achievement.first_claim_feed": "Feed Owner",
"achievement.first_claim_feed_description": "You own your feed on Follow",
"achievement.mint_more_power": "Be a hardcore player and earn more <power />",
"activation.activate": "Activate",
"activation.description": "During the public beta, users who have not used their activation code can only use some of the features.",
"activation.title": "Invitation Code",
"ai_daily.header": "AI Daily Report",
"ai_daily.no_found": "No AI news found for this period.",
"ai_daily.title": "Top News - {{title}}",