One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links ๐Ÿ“… calendar.xyehr.cn
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #142 from EvanTechDev/codex/update-authwaitingloading-component

fix: small fix

authored by

Evan Huang and committed by
GitHub
a9eb8257 cd6f467e

+112 -37
+35 -23
app/(app)/app/page.tsx
··· 1 1 "use client" 2 + 3 + import Calendar from "@/components/app/calendar" 2 4 import AuthWaitingLoading from "@/components/app/auth-waiting-loading" 3 - import dynamic from "next/dynamic" 4 - import { useRouter } from "next/navigation" 5 5 import { useUser } from "@clerk/nextjs" 6 - import { useEffect } from "react" 6 + import { useEffect, useMemo, useState } from "react" 7 + 8 + const AUTH_FLOW_ROUTES = ["/sign-in", "/sign-up", "/reset-password", "/sso-callback"] 9 + 10 + function isFromAuthFlow(referrer: string) { 11 + if (!referrer) return false 7 12 8 - const Calendar = dynamic(() => import("@/components/app/calendar"), { 9 - loading: () => <AuthWaitingLoading />, 10 - }) 13 + try { 14 + const refUrl = new URL(referrer) 15 + return AUTH_FLOW_ROUTES.some((route) => refUrl.pathname.includes(route)) 16 + } catch { 17 + return false 18 + } 19 + } 11 20 12 21 export default function Home() { 13 - const { isLoaded, isSignedIn } = useUser() 14 - const router = useRouter() 15 - const isAuthBypassEnabled = 16 - process.env.NEXT_PUBLIC_BYPASS_CLERK_AUTH === "true" || 17 - process.env.NEXT_PUBLIC_VERCEL_ENV === "preview" 22 + const { isLoaded } = useUser() 23 + const [cameFromAuthFlow, setCameFromAuthFlow] = useState(false) 24 + const [showDelayedWait, setShowDelayedWait] = useState(false) 18 25 19 26 useEffect(() => { 20 - if (isAuthBypassEnabled) return 27 + setCameFromAuthFlow(isFromAuthFlow(document.referrer)) 28 + }, []) 21 29 22 - if (isLoaded && !isSignedIn) { 23 - router.replace("/sign-in") 30 + useEffect(() => { 31 + if (!cameFromAuthFlow || isLoaded) { 32 + setShowDelayedWait(false) 33 + return 24 34 } 25 - }, [isLoaded, isSignedIn, isAuthBypassEnabled, router]) 35 + 36 + const timer = window.setTimeout(() => { 37 + setShowDelayedWait(true) 38 + }, 300) 39 + 40 + return () => window.clearTimeout(timer) 41 + }, [cameFromAuthFlow, isLoaded]) 26 42 27 - if (isAuthBypassEnabled) { 28 - return <Calendar /> 29 - } 43 + const shouldShowAuthWait = useMemo(() => { 44 + return cameFromAuthFlow && !isLoaded && showDelayedWait 45 + }, [cameFromAuthFlow, isLoaded, showDelayedWait]) 30 46 31 - if (!isLoaded) { 47 + if (shouldShowAuthWait) { 32 48 return <AuthWaitingLoading /> 33 - } 34 - 35 - if (!isSignedIn) { 36 - return null 37 49 } 38 50 39 51 return <Calendar />
+4 -6
components/app/analytics/share-management.tsx
··· 7 7 import { useEffect, useState } from "react"; 8 8 import { format } from "date-fns"; 9 9 import { toast } from "sonner"; 10 + import { fetchJson } from "@/lib/fetch-json" 10 11 11 12 interface SharedEvent { 12 13 id: string; ··· 35 36 useEffect(() => { 36 37 async function fetchSharedEvents() { 37 38 try { 38 - const res = await fetch("/api/share/list"); 39 - if (!res.ok) throw new Error("Failed to fetch shared events"); 40 - const data = await res.json(); 41 - setSharedEvents(data.shares || []); 39 + const data = await fetchJson<{ shares?: SharedEvent[] }>("/api/share/list") 40 + setSharedEvents(data.shares || []) 42 41 } catch (error) { 43 42 console.error("Error fetching shared events:", error); 44 43 toast.error(t.shareManagementLoadFailed, { ··· 93 92 try { 94 93 setIsDecrypting(true) 95 94 96 - const res = await fetch( 95 + const data = await fetchJson<{ success: boolean; data: string }>( 97 96 `/api/share?id=${decryptingShare.id}&password=${encodeURIComponent(passwordInput)}` 98 97 ) 99 - const data = await res.json() 100 98 101 99 if (!data.success) { 102 100 toast.error(t.invalidPassword)
+1 -1
components/app/auth-waiting-loading.tsx
··· 22 22 return ( 23 23 <div className="flex min-h-screen items-center justify-center bg-white px-6 dark:bg-black"> 24 24 <div className="flex flex-col items-center gap-5 text-center"> 25 - <Image src="/icon.svg" alt="One Calendar" width={84} height={84} priority /> 25 + <Image src="/icon.svg" alt="One Calendar" width={128} height={128} priority /> 26 26 <p className="text-sm text-slate-700 dark:text-slate-300"> 27 27 {t.loadingCalendar}{".".repeat(dotCount)} 28 28 </p>
+34 -6
components/app/calendar.tsx
··· 31 31 AlertDialogTitle, 32 32 } from "@/components/ui/alert-dialog" 33 33 34 - const DayView = dynamic(() => import("@/components/app/views/day-view")) 35 - const WeekView = dynamic(() => import("@/components/app/views/week-view")) 36 - const MonthView = dynamic(() => import("@/components/app/views/month-view")) 37 - const YearView = dynamic(() => import("@/components/app/views/year-view")) 38 - const AnalyticsView = dynamic(() => import("@/components/app/analytics/analytics-view")) 39 - const Settings = dynamic(() => import("@/components/app/profile/settings")) 34 + const loadDayView = () => import("@/components/app/views/day-view") 35 + const loadWeekView = () => import("@/components/app/views/week-view") 36 + const loadMonthView = () => import("@/components/app/views/month-view") 37 + const loadYearView = () => import("@/components/app/views/year-view") 38 + const loadAnalyticsView = () => import("@/components/app/analytics/analytics-view") 39 + const loadSettings = () => import("@/components/app/profile/settings") 40 + 41 + const DayView = dynamic(loadDayView) 42 + const WeekView = dynamic(loadWeekView) 43 + const MonthView = dynamic(loadMonthView) 44 + const YearView = dynamic(loadYearView) 45 + const AnalyticsView = dynamic(loadAnalyticsView) 46 + const Settings = dynamic(loadSettings) 40 47 41 48 type ViewType = "day" | "week" | "four-day" | "month" | "year" | "analytics" | "settings" 42 49 ··· 109 116 if (view !== defaultView) { 110 117 setView(defaultView as ViewType) 111 118 } 119 + }, []) 120 + 121 + useEffect(() => { 122 + const prefetch = () => { 123 + void loadDayView() 124 + void loadWeekView() 125 + void loadMonthView() 126 + void loadYearView() 127 + void loadAnalyticsView() 128 + void loadSettings() 129 + } 130 + 131 + if (typeof window === "undefined") return 132 + 133 + if ("requestIdleCallback" in window) { 134 + const id = window.requestIdleCallback(prefetch) 135 + return () => window.cancelIdleCallback(id) 136 + } 137 + 138 + const timeoutId = window.setTimeout(prefetch, 800) 139 + return () => window.clearTimeout(timeoutId) 112 140 }, []) 113 141 114 142 // Add the keyboard shortcut handler
+32
lib/fetch-json.ts
··· 1 + const inflight = new Map<string, Promise<unknown>>() 2 + 3 + export async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> { 4 + const method = (init?.method ?? "GET").toUpperCase() 5 + const key = `${method}:${url}:${init?.body ? String(init.body) : ""}` 6 + 7 + if (method === "GET" && inflight.has(key)) { 8 + return inflight.get(key) as Promise<T> 9 + } 10 + 11 + const request = fetch(url, { 12 + ...init, 13 + headers: { 14 + Accept: "application/json", 15 + ...(init?.headers ?? {}), 16 + }, 17 + }).then(async (response) => { 18 + if (!response.ok) { 19 + const error = new Error(`Request failed: ${response.status}`) 20 + ;(error as Error & { status?: number }).status = response.status 21 + throw error 22 + } 23 + return (await response.json()) as T 24 + }) 25 + 26 + if (method === "GET") { 27 + inflight.set(key, request) 28 + request.finally(() => inflight.delete(key)) 29 + } 30 + 31 + return request 32 + }
+6 -1
next.config.mjs
··· 24 24 ignoreBuildErrors: true, 25 25 }, 26 26 images: { 27 - unoptimized: true, 27 + formats: ["image/avif", "image/webp"], 28 + minimumCacheTTL: 60 * 60 * 24, 28 29 }, 29 30 env: { 30 31 NEXT_PUBLIC_APP_VERSION: packageJson.version, 31 32 NEXT_PUBLIC_GIT_COMMIT: getGitCommit(), 32 33 NEXT_PUBLIC_BUILD_TIME: new Date().toISOString(), 34 + }, 35 + 36 + experimental: { 37 + optimizePackageImports: ["lucide-react", "date-fns", "@radix-ui/react-icons", "recharts"], 33 38 }, 34 39 35 40 async headers() {