s''
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
pbSaveAuth,
|
||||
} from "@/app/lib/pocketbase";
|
||||
import { useTranslation } from "@/i18n/navigation";
|
||||
import { apiUrl } from "@/app/lib/api-client";
|
||||
|
||||
interface AuthModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -48,7 +49,7 @@ export default function AuthModal({ isOpen, onClose, onSuccess }: AuthModalProps
|
||||
}
|
||||
pbSaveAuth(result.token, result.record);
|
||||
try {
|
||||
await fetch("/api/auth/sync-session", {
|
||||
await fetch(apiUrl("/api/auth/sync-session"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token: result.token, record: result.record }),
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "@/i18n/navigation";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
import { City } from "@/app/data/cities";
|
||||
import { apiFetch } from "@/app/lib/api-client";
|
||||
|
||||
const CITY_COORDS: Record<string, [number, number]> = {
|
||||
大理: [25.7, 100.2],
|
||||
@@ -62,18 +64,34 @@ interface CityModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface CityDetailData {
|
||||
detail?: Record<string, unknown> | null;
|
||||
content?: Array<Record<string, unknown>>;
|
||||
meetups?: Array<Record<string, unknown>>;
|
||||
discussions?: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export default function CityModal({ city, isOpen, onClose }: CityModalProps) {
|
||||
const { t } = useTranslation("cityModal");
|
||||
const [activeTab, setActiveTab] = useState("scores");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [favorited, setFavorited] = useState(false);
|
||||
const [detailData, setDetailData] = useState<CityDetailData | null>(null);
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!city || !isOpen) return;
|
||||
const slug = city.slug || city.nameEn?.toLowerCase() || city.name.toLowerCase();
|
||||
apiFetch<CityDetailData & { ok: boolean }>(`/api/cities/${slug}`)
|
||||
.then((data) => setDetailData(data))
|
||||
.catch(() => setDetailData(null));
|
||||
}, [city, isOpen]);
|
||||
|
||||
if (!isOpen || !city || !mounted) return null;
|
||||
|
||||
const coverImage = city.coverImage ?? CITY_IMAGES[city.name] ?? `linear-gradient(135deg, ${city.gradientFrom}, ${city.gradientTo})`;
|
||||
const reviewsCount = city.reviewsCount ?? city.nomadsNow * 4 + Math.floor(Math.random() * 500);
|
||||
const reviewsCount = city.reviewsCount ?? city.nomadsNow * 4 + ((city.id * 97) % 500);
|
||||
const likedCount = city.likedCount ?? Math.floor(reviewsCount * 0.55);
|
||||
const dislikedCount = city.dislikedCount ?? Math.floor(reviewsCount * 0.08);
|
||||
const qualityOfLife = (city.qualityOfLife?.toLowerCase() as ScoreLabel) ?? scoreToLabel(city.overallScore);
|
||||
@@ -249,12 +267,7 @@ export default function CityModal({ city, isOpen, onClose }: CityModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-6 sm:py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<p className="text-lg">{city.icon}</p>
|
||||
<p className="mt-2 text-sm sm:text-base">
|
||||
「{tabs.find((x) => x.id === activeTab) ? t(tabs.find((x) => x.id === activeTab)!.labelKey) : activeTab}」{t("comingSoon")}
|
||||
</p>
|
||||
</div>
|
||||
<CityTabContent activeTab={activeTab} city={city} data={detailData} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,3 +309,287 @@ function getAcLabel(pct: number, t: (k: string) => string): string {
|
||||
if (pct >= 70) return t("scores.high");
|
||||
return t("scores.normal");
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
function asArray(value: unknown): Array<Record<string, unknown>> {
|
||||
return Array.isArray(value) ? (value.filter((x) => x && typeof x === "object") as Array<Record<string, unknown>>) : [];
|
||||
}
|
||||
|
||||
function asStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.map(String) : [];
|
||||
}
|
||||
|
||||
function CityTabContent({
|
||||
activeTab,
|
||||
city,
|
||||
data,
|
||||
}: {
|
||||
activeTab: string;
|
||||
city: City;
|
||||
data: CityDetailData | null;
|
||||
}) {
|
||||
const detail = asRecord(data?.detail);
|
||||
const tab = asRecord(detail[activeTab]);
|
||||
const content = data?.content || [];
|
||||
const meetups = data?.meetups || [];
|
||||
const discussions = data?.discussions || [];
|
||||
|
||||
if (activeTab === "guide") {
|
||||
return (
|
||||
<div className="grid gap-5 lg:grid-cols-[1fr_280px]">
|
||||
<section className="space-y-4">
|
||||
<Panel title="城市落地指南">
|
||||
<p className="leading-7 text-gray-700 dark:text-gray-300">{String(tab.summary || city.description)}</p>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
<InfoCard label="工作配置" value={String(tab.workSetup || `${city.internetSpeed}Mbps 网络,建议先短住实测。`)} />
|
||||
<InfoCard label="适合人群" value={asStringArray(tab.bestFor).join(" · ") || city.tags.join(" · ")} />
|
||||
</div>
|
||||
</Panel>
|
||||
<Panel title="30 天落地清单">
|
||||
<CheckList items={asStringArray(tab.arrivalChecklist)} />
|
||||
</Panel>
|
||||
</section>
|
||||
<section className="space-y-4">
|
||||
<RelatedContent items={content} />
|
||||
<Panel title="同城活动">
|
||||
<div className="space-y-3">
|
||||
{meetups.map((meetup) => (
|
||||
<Link key={String(meetup.id)} href="/meetups" className="block rounded-xl bg-gray-50 p-3 text-sm text-gray-700 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
<span className="font-semibold">{String(meetup.venue || "活动地点待定")}</span>
|
||||
<span className="mt-1 block text-xs text-gray-500 dark:text-gray-400">{String(meetup.date || "")} · {String(meetup.rsvpCount || 0)} 人报名</span>
|
||||
</Link>
|
||||
))}
|
||||
{meetups.length === 0 && (
|
||||
<Link href="/meetups/host" className="block rounded-xl bg-gray-50 p-3 text-sm text-gray-700 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
还没有同城活动,发起第一场聚会
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === "pros") {
|
||||
return (
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<Panel title="优点">
|
||||
<CheckList items={asStringArray(tab.pros)} tone="good" />
|
||||
</Panel>
|
||||
<Panel title="需要注意">
|
||||
<CheckList items={asStringArray(tab.cons)} tone="warn" />
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === "reviews") {
|
||||
const items = asArray(tab.items);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{items.map((review, i) => (
|
||||
<Panel key={i} title={`${review.author || "社区成员"} · ${review.role || "数字游民"}`}>
|
||||
<p className="leading-7 text-gray-700 dark:text-gray-300">{String(review.text || "")}</p>
|
||||
<p className="mt-3 text-sm text-[#ff4d4f]">评分 {String(review.score || city.overallScore)}/5</p>
|
||||
</Panel>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === "cost") {
|
||||
const breakdown = asArray(tab.breakdown);
|
||||
const total = Number(tab.monthlyTotal || city.costPerMonth);
|
||||
return (
|
||||
<Panel title={`月生活成本:¥${total.toLocaleString()}`}>
|
||||
<div className="space-y-3">
|
||||
{breakdown.map((item) => {
|
||||
const amount = Number(item.amount || 0);
|
||||
return (
|
||||
<div key={String(item.label)}>
|
||||
<div className="mb-1 flex justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>{String(item.label)}</span>
|
||||
<span>¥{amount.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div className="h-full rounded-full bg-[#ff4d4f]" style={{ width: `${Math.min(100, (amount / total) * 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400">{String(tab.tip || "成本数据会随社区反馈持续校准。")}</p>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === "people") {
|
||||
return (
|
||||
<div className="grid gap-5 lg:grid-cols-2">
|
||||
<Panel title={`${Number(tab.nomadsNow || city.nomadsNow).toLocaleString()} 位游民正在这里`}>
|
||||
<div className="space-y-3">
|
||||
{asArray(tab.personas).map((item) => (
|
||||
<InfoCard key={String(item.name)} label={String(item.name)} value={`${String(item.percent)}%`} />
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
<Panel title="相关访谈与工具">
|
||||
<RelatedContent items={content} compact />
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === "chat") {
|
||||
return (
|
||||
<div className="grid gap-5 lg:grid-cols-2">
|
||||
<Panel title="同城频道">
|
||||
<div className="space-y-3">
|
||||
{asArray(tab.channels).map((channel) => (
|
||||
<InfoCard key={String(channel.name)} label={String(channel.name)} value={`${String(channel.members)} 成员 · ${String(channel.status)}`} />
|
||||
))}
|
||||
</div>
|
||||
<Link href="/join" className="mt-4 inline-flex rounded-full bg-[#ff4d4f] px-4 py-2 text-sm font-semibold text-white">加入社群</Link>
|
||||
</Panel>
|
||||
<Panel title="同城讨论">
|
||||
<div className="space-y-3">
|
||||
{discussions.map((topic) => (
|
||||
<Link key={String(topic.id)} href="/discuss" className="block rounded-xl bg-gray-50 p-3 text-sm text-gray-700 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700">
|
||||
{String(topic.title)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === "photos") {
|
||||
const photos = asStringArray(tab.items);
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{photos.map((src) => (
|
||||
<img key={src} src={src} alt={city.name} className="h-56 w-full rounded-2xl object-cover shadow-sm" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === "weather") {
|
||||
return (
|
||||
<Panel title="天气与季节">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<InfoCard label="当前温度" value={`${String(tab.temperature || city.temperature)}°C`} />
|
||||
<InfoCard label="湿度" value={`${String(tab.humidity || city.humidity)}%`} />
|
||||
<InfoCard label="推荐月份" value={asStringArray(tab.bestMonths).join("、")} />
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400">{String(tab.note || "真实天气接口接入后会显示未来 7 天趋势。")}</p>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === "trends") {
|
||||
return (
|
||||
<Panel title="社群增长趋势">
|
||||
<div className="flex h-48 items-end gap-3">
|
||||
{asArray(tab.growth).map((point) => {
|
||||
const value = Number(point.value || 0);
|
||||
return (
|
||||
<div key={String(point.month)} className="flex flex-1 flex-col items-center gap-2">
|
||||
<div className="w-full rounded-t-lg bg-[#ff4d4f]" style={{ height: `${Math.max(18, Math.min(100, value / 12))}%` }} />
|
||||
<span className="text-xs text-gray-400">{String(point.month)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400">{String(tab.insight || "趋势数据来自社区成员位置和活动报名聚合。")}</p>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === "demographics") {
|
||||
return (
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<Panel title="年龄结构">
|
||||
{asArray(tab.age).map((item) => <InfoCard key={String(item.label)} label={String(item.label)} value={`${String(item.value)}%`} />)}
|
||||
</Panel>
|
||||
<Panel title="职业结构">
|
||||
{asArray(tab.work).map((item) => <InfoCard key={String(item.label)} label={String(item.label)} value={`${String(item.value)}%`} />)}
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <RelatedContent items={content} />;
|
||||
}
|
||||
|
||||
function Panel({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
||||
<h3 className="mb-3 font-bold text-gray-900 dark:text-gray-100">{title}</h3>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoCard({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="mb-2 rounded-xl bg-gray-50 p-3 dark:bg-gray-800">
|
||||
<p className="text-xs text-gray-400">{label}</p>
|
||||
<p className="mt-1 text-sm font-medium text-gray-800 dark:text-gray-200">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckList({ items, tone = "good" }: { items: string[]; tone?: "good" | "warn" }) {
|
||||
const icon = tone === "good" ? "✓" : "!";
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<div key={item} className="flex gap-2 rounded-xl bg-gray-50 p-3 text-sm text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
<span className={tone === "good" ? "text-emerald-500" : "text-amber-500"}>{icon}</span>
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RelatedContent({ items, compact = false }: { items: Array<Record<string, unknown>>; compact?: boolean }) {
|
||||
const hrefFor = (item: Record<string, unknown>) => {
|
||||
const type = String(item.type || "");
|
||||
const slug = String(item.slug || "");
|
||||
if (type === "ebook") return `/ebooks/${slug}`;
|
||||
if (type === "video") return `/videos/${slug}`;
|
||||
if (type === "app") return "/app";
|
||||
return String(item.targetUrl || "https://digital.hackrobot.cn/zh");
|
||||
};
|
||||
return (
|
||||
<Panel title="关联内容">
|
||||
<div className="space-y-3">
|
||||
{items.map((item) => {
|
||||
const href = hrefFor(item);
|
||||
const external = href.startsWith("http");
|
||||
const body = (
|
||||
<div className="flex gap-3 rounded-xl bg-gray-50 p-3 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700">
|
||||
{!compact && <img src={String(item.coverImage || "")} alt="" className="h-14 w-20 rounded-lg object-cover" />}
|
||||
<div className="min-w-0">
|
||||
<p className="line-clamp-1 text-sm font-semibold text-gray-900 dark:text-gray-100">{String(item.title)}</p>
|
||||
<p className="mt-1 line-clamp-2 text-xs text-gray-500 dark:text-gray-400">{String(item.subtitle || "")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return external ? (
|
||||
<a key={String(item.slug)} href={href} target="_blank" rel="noopener noreferrer">{body}</a>
|
||||
) : (
|
||||
<Link key={String(item.slug)} href={href}>{body}</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getDeviceFromEnv,
|
||||
} from "@/app/lib/payment";
|
||||
import type { PayChannel, PayEnv } from "@/app/lib/payment";
|
||||
import { apiUrl } from "@/app/lib/api-client";
|
||||
|
||||
const MEDIA_NAMES_ZH = ["36氪", "少数派", "极客公园", "虎嗅", "创业邦", "澎湃新闻"];
|
||||
const MEDIA_NAMES_EN = ["36Kr", "sspai", "GeekPark", "Huxiu", "Cyzone", "The Paper"];
|
||||
@@ -72,7 +73,7 @@ export default function HeroSection() {
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/auth/me", { credentials: "include" });
|
||||
const res = await fetch(apiUrl("/api/auth/me"), { credentials: "include" });
|
||||
const data = await res.json();
|
||||
setIsLoggedIn(!!data?.user);
|
||||
} catch {
|
||||
@@ -108,7 +109,7 @@ export default function HeroSection() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const checkRes = await fetch("/api/meetup/check-user", {
|
||||
const checkRes = await fetch(apiUrl("/api/meetup/check-user"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: trimmed }),
|
||||
@@ -135,7 +136,7 @@ export default function HeroSection() {
|
||||
isPrivateDevHost(window.location.hostname);
|
||||
let user_id = buildPendingUserId(trimmed);
|
||||
if (shouldPrecreateUser) {
|
||||
const ensureRes = await fetch("/api/meetup/ensure-user", {
|
||||
const ensureRes = await fetch(apiUrl("/api/meetup/ensure-user"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: trimmed }),
|
||||
@@ -150,11 +151,11 @@ export default function HeroSection() {
|
||||
);
|
||||
}
|
||||
if (ensureData.is_new) {
|
||||
await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" });
|
||||
await fetch(apiUrl("/api/meetup/set-needs-password-change"), { method: "POST", credentials: "include" });
|
||||
}
|
||||
user_id = String(ensureData.user_id).trim();
|
||||
if (ensureData?.token && ensureData?.record) {
|
||||
await fetch("/api/auth/sync-session", {
|
||||
await fetch(apiUrl("/api/auth/sync-session"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token: ensureData.token, record: ensureData.record }),
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getDeviceFromEnv,
|
||||
} from "@/app/lib/payment";
|
||||
import type { PayChannel, PayEnv } from "@/app/lib/payment";
|
||||
import { apiUrl } from "@/app/lib/api-client";
|
||||
|
||||
interface JoinModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -44,7 +45,7 @@ export default function JoinModal({ isOpen, onClose, initialEmail = "", vipOnly
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/meetup/ensure-user", {
|
||||
const res = await fetch(apiUrl("/api/meetup/ensure-user"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: em, password: password || undefined }),
|
||||
@@ -55,11 +56,11 @@ export default function JoinModal({ isOpen, onClose, initialEmail = "", vipOnly
|
||||
throw new Error(data?.error || (locale === "zh" ? "操作失败" : "Operation failed"));
|
||||
}
|
||||
if (data.is_new) {
|
||||
await fetch("/api/meetup/set-needs-password-change", { method: "POST", credentials: "include" });
|
||||
await fetch(apiUrl("/api/meetup/set-needs-password-change"), { method: "POST", credentials: "include" });
|
||||
}
|
||||
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
|
||||
pbSaveAuth(data.token, data.record);
|
||||
await fetch("/api/auth/sync-session", {
|
||||
await fetch(apiUrl("/api/auth/sync-session"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token: data.token, record: data.record }),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Link, useLocale, usePathname, useRouter, useTranslation } from "@/i18n/navigation";
|
||||
import { getStoredUserEmail, getStoredUser, getStoredToken, pbLogout } from "@/app/lib/pocketbase";
|
||||
import { apiUrl } from "@/app/lib/api-client";
|
||||
import { getNavLinks } from "@/config";
|
||||
import AuthModal from "./AuthModal";
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
@@ -25,7 +26,7 @@ export default function NavbarComponent() {
|
||||
|
||||
const fetchAuth = useCallback(async () => {
|
||||
try {
|
||||
let res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
|
||||
let res = await fetch(apiUrl("/api/auth/me"), { credentials: "include", cache: "no-store" });
|
||||
let data = await res.json();
|
||||
if (data?.user && data?.token) {
|
||||
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
|
||||
@@ -37,14 +38,14 @@ export default function NavbarComponent() {
|
||||
const token = getStoredToken();
|
||||
const record = getStoredUser();
|
||||
if (token && record?.id && record?.email) {
|
||||
const syncRes = await fetch("/api/auth/sync-session", {
|
||||
const syncRes = await fetch(apiUrl("/api/auth/sync-session"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token, record }),
|
||||
credentials: "include",
|
||||
});
|
||||
if (syncRes.ok) {
|
||||
res = await fetch("/api/auth/me", { credentials: "include", cache: "no-store" });
|
||||
res = await fetch(apiUrl("/api/auth/me"), { credentials: "include", cache: "no-store" });
|
||||
data = await res.json();
|
||||
if (data?.user && data?.token) {
|
||||
setUserEmail(data.user.email);
|
||||
|
||||
Reference in New Issue
Block a user