feat: release v0.1.3 chat, profiles, and media
Keep read receipts for the other person only, and let photos, voice, and files stay cached until they are actually replaced.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CheckCircle2, Loader2, RefreshCw, ShieldCheck, Timer, Zap } from "lucide-react";
|
||||
import { CheckCircle2, Loader2, ShieldCheck, Timer, Zap } from "lucide-react";
|
||||
import { BrandMark } from "@/components/brand-mark";
|
||||
import { useAuth } from "@/lib/auth-store";
|
||||
import { http, toUserError } from "@/lib/api";
|
||||
@@ -15,6 +15,7 @@ import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { CaptchaField, type CaptchaChallenge } from "@/components/captcha-field";
|
||||
|
||||
const perks = [
|
||||
{ icon: Timer, text: "زمان تماشای راستیآزماییشده — بدون میانبر" },
|
||||
@@ -22,11 +23,6 @@ const perks = [
|
||||
{ icon: CheckCircle2, text: "لیگ هفتگی، رویداد زنده و منتور یکبهیک" },
|
||||
];
|
||||
|
||||
type CaptchaChallenge = {
|
||||
challengeId: string;
|
||||
imageBase64: string;
|
||||
};
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const setSession = useAuth((s) => s.setSession);
|
||||
@@ -34,7 +30,7 @@ export default function LoginPage() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [captchaAnswer, setCaptchaAnswer] = useState("");
|
||||
const [challenge, setChallenge] = useState<CaptchaChallenge | null>(null);
|
||||
const [captchaLoading, setCaptchaLoading] = useState(true);
|
||||
const [captchaKey, setCaptchaKey] = useState(0);
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -44,29 +40,11 @@ export default function LoginPage() {
|
||||
queryFn: () => http.get<{ enabled: boolean }>("/api/auth/register-status"),
|
||||
});
|
||||
|
||||
const loadCaptcha = useCallback(async () => {
|
||||
setCaptchaLoading(true);
|
||||
setCaptchaAnswer("");
|
||||
try {
|
||||
const data = await http.get<CaptchaChallenge>("/api/auth/captcha");
|
||||
setChallenge(data);
|
||||
} catch (err) {
|
||||
setChallenge(null);
|
||||
setError(toUserError(err, "بارگذاری کد امنیتی ممکن نشد"));
|
||||
} finally {
|
||||
setCaptchaLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadCaptcha();
|
||||
}, [loadCaptcha]);
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!challenge) {
|
||||
setError("ابتدا کد امنیتی را بارگذاری کنید");
|
||||
void loadCaptcha();
|
||||
setCaptchaKey((k) => k + 1);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -83,7 +61,7 @@ export default function LoginPage() {
|
||||
router.push("/cap");
|
||||
} catch (err) {
|
||||
setError(toUserError(err, "ورود ناموفق بود"));
|
||||
void loadCaptcha();
|
||||
setCaptchaKey((k) => k + 1);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -153,54 +131,14 @@ export default function LoginPage() {
|
||||
placeholder="رمز عبور"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label htmlFor="captcha">کد امنیتی</Label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void loadCaptcha()}
|
||||
disabled={captchaLoading}
|
||||
className="inline-flex items-center gap-1 text-xs font-medium text-primary underline-offset-4 hover:underline disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`h-3.5 w-3.5 ${captchaLoading ? "animate-spin" : ""}`} />
|
||||
تازهسازی
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-stretch gap-3">
|
||||
<div
|
||||
className="relative flex h-14 min-w-[9.5rem] shrink-0 items-center justify-center overflow-hidden rounded-xl border border-border bg-muted/60 shadow-inner"
|
||||
aria-live="polite"
|
||||
>
|
||||
{captchaLoading || !challenge?.imageBase64 ? (
|
||||
<span className="text-sm text-muted-foreground">…</span>
|
||||
) : (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={`data:image/png;base64,${challenge.imageBase64}`}
|
||||
alt="کد امنیتی"
|
||||
width={160}
|
||||
height={52}
|
||||
className="h-full w-full object-contain select-none"
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
id="captcha"
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCapitalize="characters"
|
||||
spellCheck={false}
|
||||
value={captchaAnswer}
|
||||
onChange={(e) => setCaptchaAnswer(e.target.value.toUpperCase())}
|
||||
required
|
||||
dir="ltr"
|
||||
className="h-14 text-center font-mono tracking-widest uppercase placeholder:text-center"
|
||||
placeholder="کد تصویر"
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CaptchaField
|
||||
key={captchaKey}
|
||||
id="captcha"
|
||||
value={captchaAnswer}
|
||||
onChange={setCaptchaAnswer}
|
||||
onChallenge={setChallenge}
|
||||
onError={setError}
|
||||
/>
|
||||
<label className="flex items-center justify-between gap-3 text-sm">
|
||||
<span>مرا به یاد داشته باش</span>
|
||||
<Switch
|
||||
@@ -215,7 +153,7 @@ export default function LoginPage() {
|
||||
variant="gradient"
|
||||
className="h-12 w-full"
|
||||
size="lg"
|
||||
disabled={loading || captchaLoading || !challenge}
|
||||
disabled={loading || !challenge}
|
||||
>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Zap className="h-4 w-4" />} ورود
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user