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.

Use shared Spinner and move db tables check API route

+56 -40
app/[handle]/[shareId]/page.tsx app/(app)/[handle]/[shareId]/page.tsx
app/api/db/tables/route.ts app/api/blob/check/route.ts
app/at-oauth/page.tsx app/(auth)/at-oauth/page.tsx
+2 -13
components/app/event/event-preview.tsx
··· 33 33 } from "@/components/ui/dialog"; 34 34 import { Input } from "@/components/ui/input"; 35 35 import { Label } from "@/components/ui/label"; 36 + import { Spinner } from "@/components/ui/spinner"; 36 37 import { toast } from "sonner"; 37 38 import QRCodeStyling from "qr-code-styling"; 38 39 import { useUser } from "@clerk/nextjs"; ··· 681 682 > 682 683 {isSharing ? ( 683 684 <span className="flex items-center"> 684 - <svg 685 - className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" 686 - xmlns="http://www.w3.org/2000/svg" 687 - fill="none" 688 - viewBox="0 0 24 24" 689 - > 690 - <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> 691 - <path 692 - className="opacity-75" 693 - fill="currentColor" 694 - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 695 - ></path> 696 - </svg> 685 + <Spinner className="mr-2" /> 697 686 {t.shareSharing} 698 687 </span> 699 688 ) : (
+17 -7
components/app/profile/settings.tsx
··· 67 67 const timeZoneName = parts.find((part) => part.type === "timeZoneName")?.value ?? "" 68 68 69 69 if (timeZoneName === "GMT" || timeZoneName === "UTC") { 70 - return "UTC+00:00" 70 + return { offsetString: "UTC+00:00", offsetMinutes: 0 } 71 71 } 72 72 73 73 const match = timeZoneName.match(/(?:GMT|UTC)([+-])(\d{1,2})(?::?(\d{2}))?/) 74 74 if (!match) { 75 - return "UTC+00:00" 75 + return { offsetString: "UTC+00:00", offsetMinutes: 0 } 76 76 } 77 77 78 78 const [, sign, hours, minutes = "00"] = match 79 - return `UTC${sign}${hours.padStart(2, "0")}:${minutes}` 79 + const parsedHours = Number.parseInt(hours, 10) 80 + const parsedMinutes = Number.parseInt(minutes, 10) 81 + const totalMinutes = parsedHours * 60 + parsedMinutes 82 + const offsetMinutes = sign === "-" ? -totalMinutes : totalMinutes 83 + 84 + return { 85 + offsetString: `UTC${sign}${hours.padStart(2, "0")}:${minutes}`, 86 + offsetMinutes, 87 + } 80 88 } 81 89 82 90 return timezones 83 91 .map((tz) => { 84 92 try { 85 - const offsetString = getUTCOffset(tz) 93 + const { offsetString, offsetMinutes } = getUTCOffset(tz) 86 94 87 95 return { 88 96 value: tz, 89 - label: `${tz} (${offsetString})`, 97 + label: `${offsetString} ยท ${tz}`, 98 + offsetMinutes, 90 99 } 91 100 } catch { 92 101 return { 93 102 value: tz, 94 - label: tz, 103 + label: `UTC+00:00 ยท ${tz}`, 104 + offsetMinutes: 0, 95 105 } 96 106 } 97 107 }) 98 - .sort((a, b) => a.label.localeCompare(b.label)) 108 + .sort((a, b) => a.offsetMinutes - b.offsetMinutes || a.value.localeCompare(b.value)) 99 109 } 100 110 101 111 const gmtTimezones = getGMTTimezones()
+2 -13
components/app/profile/user-profile-button.tsx
··· 41 41 import { Input } from "@/components/ui/input" 42 42 import { Label } from "@/components/ui/label" 43 43 import { ScrollArea } from "@/components/ui/scroll-area" 44 + import { Spinner } from "@/components/ui/spinner" 44 45 import { toast } from "sonner" 45 46 import { useCalendar } from "@/components/providers/calendar-context" 46 47 import { translations, useLanguage } from "@/lib/i18n" ··· 916 917 <Button onClick={unlock} disabled={isUnlocking}> 917 918 {isUnlocking ? ( 918 919 <span className="flex items-center"> 919 - <svg 920 - className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" 921 - xmlns="http://www.w3.org/2000/svg" 922 - fill="none" 923 - viewBox="0 0 24 24" 924 - > 925 - <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> 926 - <path 927 - className="opacity-75" 928 - fill="currentColor" 929 - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 930 - ></path> 931 - </svg> 920 + <Spinner className="mr-2" /> 932 921 {t.verifying} 933 922 </span> 934 923 ) : (
+18 -6
components/app/sidebar/countdown.tsx
··· 69 69 { value: "bg-orange-500", labelKey: "colorOrange" }, 70 70 ]; 71 71 72 + const parseDateString = (dateStr: string) => { 73 + const [year, month, day] = dateStr.split("-").map(Number); 74 + return new Date(year, month - 1, day); 75 + }; 76 + 77 + const toDateString = (date: Date) => { 78 + const year = date.getFullYear(); 79 + const month = String(date.getMonth() + 1).padStart(2, "0"); 80 + const day = String(date.getDate()).padStart(2, "0"); 81 + return `${year}-${month}-${day}`; 82 + }; 83 + 72 84 export function CountdownTool({ open, onOpenChange }: CountdownToolProps) { 73 85 const [countdowns, setCountdowns] = useLocalStorage<Countdown[]>( 74 86 "countdowns", ··· 91 103 const [calendarOpen, setCalendarOpen] = useState(false); 92 104 93 105 const formatDate = (dateStr: string) => { 94 - const date = new Date(dateStr); 106 + const date = parseDateString(dateStr); 95 107 const options: Intl.DateTimeFormatOptions = { 96 108 year: "numeric", 97 109 month: "short", ··· 101 113 }; 102 114 103 115 const formatDateLong = (dateStr: string) => { 104 - const date = new Date(dateStr); 116 + const date = parseDateString(dateStr); 105 117 const options: Intl.DateTimeFormatOptions = { 106 118 year: "numeric", 107 119 month: "long", ··· 114 126 const today = new Date(); 115 127 today.setHours(0, 0, 0, 0); 116 128 117 - const targetDate = new Date(dateStr); 129 + const targetDate = parseDateString(dateStr); 118 130 let nextDate = new Date( 119 131 today.getFullYear(), 120 132 targetDate.getMonth(), ··· 149 161 150 162 const getTodayDateString = () => { 151 163 const today = new Date(); 152 - return today.toISOString().split("T")[0]; 164 + return toDateString(today); 153 165 }; 154 166 155 167 const tRepeat = (key: Countdown["repeat"]) => ··· 181 193 182 194 const startEditCountdown = (countdown: Countdown) => { 183 195 setNewCountdown(countdown); 184 - setSelectedDate(new Date(countdown.date)); 196 + setSelectedDate(parseDateString(countdown.date)); 185 197 setView("edit"); 186 198 }; 187 199 ··· 196 208 const countdown: Countdown = { 197 209 id: selectedCountdown?.id || Date.now().toString(), 198 210 name: newCountdown.name, 199 - date: selectedDate.toISOString().split("T")[0], 211 + date: toDateString(selectedDate), 200 212 repeat: newCountdown.repeat || "none", 201 213 description: newCountdown.description || "", 202 214 color: newCountdown.color,
+16
components/ui/spinner.tsx
··· 1 + import { LoaderIcon } from "lucide-react" 2 + 3 + import { cn } from "@/lib/utils" 4 + 5 + function Spinner({ className, ...props }: React.ComponentProps<"svg">) { 6 + return ( 7 + <LoaderIcon 8 + role="status" 9 + aria-label="Loading" 10 + className={cn("size-4 animate-spin", className)} 11 + {...props} 12 + /> 13 + ) 14 + } 15 + 16 + export { Spinner }
+1 -1
vercel.json
··· 1 1 { 2 2 "crons": [ 3 3 { 4 - "path": "/api/db/tables", 4 + "path": "/api/blob/check", 5 5 "schedule": "0 0 * * *" 6 6 } 7 7 ]