parent
eb2613114b
commit
b69c6f97c8
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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],
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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}}",
|
||||
|
|
|
|||
Loading…
Reference in New Issue