diff --git a/package.json b/package.json
index e724187..052d031 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "my-weekly-todo-list",
- "version": "1.55.3",
+ "version": "1.56.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js",
"scripts": {
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index d1e0880..8ccc3e1 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -116,6 +116,7 @@ model User {
weatherLon Float?
weatherLocation String?
weatherRecentCities Json?
+ hasCompletedOnboarding Boolean @default(true)
notificationsEnabled Boolean @default(false)
somedayLists SomedayList[]
tasks Task[]
diff --git a/src/app/api/auth/signup/route.ts b/src/app/api/auth/signup/route.ts
index 091b3a0..6430e54 100644
--- a/src/app/api/auth/signup/route.ts
+++ b/src/app/api/auth/signup/route.ts
@@ -80,6 +80,7 @@ export async function POST(request: NextRequest) {
emailVerificationCode: code,
emailVerificationToken: token,
emailVerificationExpires: expires,
+ hasCompletedOnboarding: false,
// emailVerified is NOT set — user must verify
},
select: {
diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts
index 4046454..c09e956 100644
--- a/src/app/api/user/profile/route.ts
+++ b/src/app/api/user/profile/route.ts
@@ -103,6 +103,7 @@ export async function GET(request: NextRequest) {
weatherLocation: true,
weatherRecentCities: true,
viewSettings: true,
+ hasCompletedOnboarding: true,
createdAt: true
}
});
@@ -146,7 +147,8 @@ export async function PATCH(request: NextRequest) {
showTaskCheckboxes, dayHeaderGap,
showCompletedTasks, showLines, startDayOffset, weekdayFormat, weekdayCase, customWeekdayNames, quoteSourceUrls, quoteLanguages,
kanbanStages, lightTheme, darkTheme, notificationsEnabled, mobileFontScale,
- weatherEnabled, weatherLat, weatherLon, weatherLocation, weatherRecentCities, viewSettings
+ weatherEnabled, weatherLat, weatherLon, weatherLocation, weatherRecentCities, viewSettings,
+ hasCompletedOnboarding
} = body;
const updateData: any = {
@@ -238,6 +240,7 @@ export async function PATCH(request: NextRequest) {
...(weatherLocation !== undefined && { weatherLocation }),
...(weatherRecentCities !== undefined && { weatherRecentCities }),
...(viewSettings !== undefined && { viewSettings }),
+ ...(hasCompletedOnboarding !== undefined && { hasCompletedOnboarding }),
};
if (password && password.trim() !== "") {
updateData.passwordHash = await bcrypt.hash(password, 10);
@@ -338,6 +341,7 @@ export async function PATCH(request: NextRequest) {
weatherLocation: true,
weatherRecentCities: true,
viewSettings: true,
+ hasCompletedOnboarding: true,
}
});
diff --git a/src/app/globals.css b/src/app/globals.css
index 6029a7f..5d2a02b 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -4861,3 +4861,56 @@ h3 {
.time-grid-on .time-column-header {
z-index: 45 !important;
}
+
+/* ============================================
+ ONBOARDING WIZARD
+ ============================================ */
+.onboarding-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(4px);
+ z-index: 2000;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.onboarding-card {
+ border-radius: 16px;
+ width: 90%;
+ max-width: 560px;
+ max-height: 85vh;
+ overflow-y: auto;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ animation: onboarding-appear 0.3s ease-out;
+ position: relative;
+}
+
+@keyframes onboarding-appear {
+ from { opacity: 0; transform: scale(0.95) translateY(10px); }
+ to { opacity: 1; transform: scale(1) translateY(0); }
+}
+
+.onboarding-step-forward { animation: ob-slide-right 0.25s ease-out; }
+.onboarding-step-backward { animation: ob-slide-left 0.25s ease-out; }
+
+@keyframes ob-slide-right {
+ from { opacity: 0; transform: translateX(30px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
+@keyframes ob-slide-left {
+ from { opacity: 0; transform: translateX(-30px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
+@media (max-width: 480px) {
+ .onboarding-card {
+ width: 100%;
+ max-width: 100%;
+ border-radius: 0;
+ max-height: 100vh;
+ height: 100vh;
+ }
+}
diff --git a/src/components/GridTaskBlock.tsx b/src/components/GridTaskBlock.tsx
index adc4878..0518141 100644
--- a/src/components/GridTaskBlock.tsx
+++ b/src/components/GridTaskBlock.tsx
@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from "react";
-import { Repeat, Circle, X } from "lucide-react";
+import { Repeat, Circle, X, ChevronDown } from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGoogle, faMicrosoft, faApple } from "@fortawesome/free-brands-svg-icons";
import { faServer } from "@fortawesome/free-solid-svg-icons";
@@ -36,6 +36,7 @@ interface GridTaskBlockProps {
projects?: any[];
onProjectAssign?: (taskId: string, projectId: string | null) => void;
kanbanStages?: KanbanStage[];
+ weatherEnabled?: boolean;
}
export function GridTaskBlock({
@@ -68,7 +69,8 @@ export function GridTaskBlock({
showTaskCheckboxes,
projects,
onProjectAssign,
- kanbanStages = []
+ kanbanStages = [],
+ weatherEnabled = false
}: GridTaskBlockProps) {
const [isNotesOpen, setIsNotesOpen] = useState(false);
const [notesValue, setNotesValue] = useState(task.markdownContent || "");
@@ -285,9 +287,11 @@ export function GridTaskBlock({
{task.subTasks && task.subTasks.length > 0 && (() => {
const completed = task.subTasks.filter(s => s.completed).length;
const total = task.subTasks.length;
+ const allDone = completed === total;
const expanded = isSubTasksOpen || isSubTaskInputOpen;
+ const pct = total > 0 ? (completed / total) * 100 : 0;
return (
-
{
e.stopPropagation();
setIsSubTasksOpen(!isSubTasksOpen);
@@ -297,25 +301,45 @@ export function GridTaskBlock({
style={{
display: "inline-flex",
alignItems: "center",
- gap: "2px",
- padding: "1px 5px",
- borderRadius: "8px",
- background: expanded ? "rgba(99, 102, 241, 0.15)" : (completed === total) ? "rgba(34, 197, 94, 0.15)" : "rgba(0,0,0,0.06)",
- color: expanded ? "#6366f1" : (completed === total) ? "#22c55e" : (darkMode ? "#aaa" : "#666"),
+ gap: "4px",
+ padding: "2px 8px 2px 4px",
+ borderRadius: "12px",
+ background: expanded ? "rgba(99,102,241,0.08)" : "transparent",
+ color: allDone ? "var(--weekly-teal, #0d9488)" : "var(--weekly-text-light, #9ca3af)",
border: "none",
cursor: "pointer",
- fontSize: "0.65rem",
+ fontSize: "0.7rem",
fontWeight: 600,
lineHeight: 1,
flexShrink: 0,
whiteSpace: "nowrap",
+ transition: "all 0.15s",
}}
>
-
- {completed}/{total}
-
+
+
+
+
+ {completed}/{total}
+
);
})()}
@@ -344,32 +368,53 @@ export function GridTaskBlock({
)}
+ {weatherEnabled && (() => {
+ const provider = task.externalProvider
+ || (task.externalId?.startsWith("synology::") ? "synology" : null);
+ if (!provider) return null;
+ const iconMap: Record = {
+ google: { icon: faGoogle, color: "#4285F4", label: "Google" },
+ outlook: { icon: faMicrosoft, color: "#0078D4", label: "Microsoft" },
+ apple: { icon: faApple, color: "#555", label: "Apple" },
+ synology: { icon: faServer, color: "#007AFF", label: "Synology" },
+ };
+ const info = iconMap[provider];
+ if (!info) return null;
+ return (
+
+
+
+ );
+ })()}
)}
- {(() => {
- const provider = task.externalProvider
- || (task.externalId?.startsWith("synology::") ? "synology" : null);
- if (!provider) return null;
- const iconMap: Record = {
- google: { icon: faGoogle, color: "#4285F4", label: "Google" },
- outlook: { icon: faMicrosoft, color: "#0078D4", label: "Microsoft" },
- apple: { icon: faApple, color: "#555", label: "Apple" },
- synology: { icon: faServer, color: "#007AFF", label: "Synology" },
- };
- const info = iconMap[provider];
- if (!info) return null;
- return (
-
-
-
- );
- })()}
+ {!weatherEnabled && (() => {
+ const provider = task.externalProvider
+ || (task.externalId?.startsWith("synology::") ? "synology" : null);
+ if (!provider) return null;
+ const iconMap: Record = {
+ google: { icon: faGoogle, color: "#4285F4", label: "Google" },
+ outlook: { icon: faMicrosoft, color: "#0078D4", label: "Microsoft" },
+ apple: { icon: faApple, color: "#555", label: "Apple" },
+ synology: { icon: faServer, color: "#007AFF", label: "Synology" },
+ };
+ const info = iconMap[provider];
+ if (!info) return null;
+ return (
+
+
+
+ );
+ })()}
void;
+ onSkip: () => void;
+ saveSetting: (key: string, value: any) => Promise
;
+ onLanguageChange: (lang: string) => void;
+ onDarkModeToggle: () => void;
+}
+
+const STEPS = ["welcome", "language", "connect", "view", "schedule", "style"] as const;
+
+const translations: Record> = {
+ en: {
+ welcomeTitle: "Welcome to My Weekly ToDo List",
+ welcomeSubtitle: "Let's set up your workspace in under a minute",
+ getStarted: "Get Started",
+ skipSetup: "Skip setup",
+ next: "Next",
+ back: "Back",
+ finish: "Start Using My Weekly ToDo List",
+ // Step 2
+ langTitle: "Language & Region",
+ langSubtitle: "Choose your language and timezone",
+ timezone: "Timezone",
+ // Step 3
+ connectTitle: "Connect Your Services",
+ connectSubtitle: "Sync calendars and tasks from your favorite services",
+ connectGoogle: "Google Calendar & Tasks",
+ connectApple: "Apple Calendar & Reminders",
+ connectOutlook: "Microsoft Outlook",
+ connectSynology: "Synology Calendar",
+ connected: "Connected",
+ connect: "Connect",
+ skipConnect: "Skip for now - you can connect later in Settings",
+ // Step 4
+ viewTitle: "Choose Your View",
+ viewSubtitle: "Pick the layout that fits how you work",
+ viewSimple: "Simple",
+ viewSimpleDesc: "Clean weekly grid with time slots",
+ viewCalendar: "Calendar",
+ viewCalendarDesc: "Google Calendar-style day view",
+ viewList: "List",
+ viewListDesc: "Focused task list without time grid",
+ viewKanban: "Kanban",
+ viewKanbanDesc: "Drag-and-drop board with stages",
+ // Step 5
+ scheduleTitle: "Your Work Hours",
+ scheduleSubtitle: "Set when your day starts and ends",
+ startHour: "Start",
+ endHour: "End",
+ viewDays: "Days per week",
+ // Step 6
+ styleTitle: "Make It Yours",
+ styleSubtitle: "Customize fonts and appearance",
+ headlineFont: "Headline font",
+ bodyFont: "Body font",
+ fontSize: "Font size",
+ darkMode: "Dark mode",
+ },
+ de: {
+ welcomeTitle: "Willkommen bei My Weekly ToDo List",
+ welcomeSubtitle: "Richte deinen Arbeitsbereich in unter einer Minute ein",
+ getStarted: "Los geht's",
+ skipSetup: "Setup uberspringen",
+ next: "Weiter",
+ back: "Zuruck",
+ finish: "My Weekly ToDo List starten",
+ langTitle: "Sprache & Region",
+ langSubtitle: "Wahle deine Sprache und Zeitzone",
+ timezone: "Zeitzone",
+ connectTitle: "Dienste verbinden",
+ connectSubtitle: "Synchronisiere Kalender und Aufgaben",
+ connectGoogle: "Google Kalender & Aufgaben",
+ connectApple: "Apple Kalender & Erinnerungen",
+ connectOutlook: "Microsoft Outlook",
+ connectSynology: "Synology Kalender",
+ connected: "Verbunden",
+ connect: "Verbinden",
+ skipConnect: "Jetzt uberspringen - du kannst spater in den Einstellungen verbinden",
+ viewTitle: "Wahle deine Ansicht",
+ viewSubtitle: "Wahle das Layout, das zu dir passt",
+ viewSimple: "Einfach",
+ viewSimpleDesc: "Ubersichtliches Wochenraster mit Zeitfenstern",
+ viewCalendar: "Kalender",
+ viewCalendarDesc: "Google Calendar-Stil Tagesansicht",
+ viewList: "Liste",
+ viewListDesc: "Fokussierte Aufgabenliste ohne Zeitraster",
+ viewKanban: "Kanban",
+ viewKanbanDesc: "Drag-and-Drop Board mit Spalten",
+ scheduleTitle: "Deine Arbeitszeiten",
+ scheduleSubtitle: "Lege fest, wann dein Tag beginnt und endet",
+ startHour: "Start",
+ endHour: "Ende",
+ viewDays: "Tage pro Woche",
+ styleTitle: "Gestalte es nach deinen Wunschen",
+ styleSubtitle: "Passe Schriftarten und Aussehen an",
+ headlineFont: "Uberschrift-Schrift",
+ bodyFont: "Text-Schrift",
+ fontSize: "Schriftgrosse",
+ darkMode: "Dunkler Modus",
+ },
+ fr: {
+ welcomeTitle: "Bienvenue sur My Weekly ToDo List",
+ welcomeSubtitle: "Configurez votre espace en moins d'une minute",
+ getStarted: "Commencer",
+ skipSetup: "Passer la configuration",
+ next: "Suivant",
+ back: "Retour",
+ finish: "Commencer a utiliser My Weekly ToDo List",
+ langTitle: "Langue & Region",
+ langSubtitle: "Choisissez votre langue et fuseau horaire",
+ timezone: "Fuseau horaire",
+ connectTitle: "Connectez vos services",
+ connectSubtitle: "Synchronisez calendriers et taches",
+ connectGoogle: "Google Agenda & Taches",
+ connectApple: "Calendrier Apple & Rappels",
+ connectOutlook: "Microsoft Outlook",
+ connectSynology: "Calendrier Synology",
+ connected: "Connecte",
+ connect: "Connecter",
+ skipConnect: "Passer pour l'instant - vous pourrez connecter plus tard",
+ viewTitle: "Choisissez votre vue",
+ viewSubtitle: "Selectionnez la mise en page qui vous convient",
+ viewSimple: "Simple",
+ viewSimpleDesc: "Grille hebdomadaire avec creneaux horaires",
+ viewCalendar: "Calendrier",
+ viewCalendarDesc: "Vue journaliere style Google Calendar",
+ viewList: "Liste",
+ viewListDesc: "Liste de taches sans grille horaire",
+ viewKanban: "Kanban",
+ viewKanbanDesc: "Tableau drag-and-drop avec colonnes",
+ scheduleTitle: "Vos horaires de travail",
+ scheduleSubtitle: "Definissez le debut et la fin de votre journee",
+ startHour: "Debut",
+ endHour: "Fin",
+ viewDays: "Jours par semaine",
+ styleTitle: "Personnalisez",
+ styleSubtitle: "Choisissez vos polices et apparence",
+ headlineFont: "Police de titre",
+ bodyFont: "Police de texte",
+ fontSize: "Taille de police",
+ darkMode: "Mode sombre",
+ },
+ es: {
+ welcomeTitle: "Bienvenido a My Weekly ToDo List",
+ welcomeSubtitle: "Configura tu espacio en menos de un minuto",
+ getStarted: "Empezar",
+ skipSetup: "Omitir configuracion",
+ next: "Siguiente",
+ back: "Atras",
+ finish: "Empezar a usar My Weekly ToDo List",
+ langTitle: "Idioma y Region",
+ langSubtitle: "Elige tu idioma y zona horaria",
+ timezone: "Zona horaria",
+ connectTitle: "Conecta tus servicios",
+ connectSubtitle: "Sincroniza calendarios y tareas",
+ connectGoogle: "Google Calendar y Tareas",
+ connectApple: "Calendario Apple y Recordatorios",
+ connectOutlook: "Microsoft Outlook",
+ connectSynology: "Calendario Synology",
+ connected: "Conectado",
+ connect: "Conectar",
+ skipConnect: "Omitir por ahora - puedes conectar despues en Ajustes",
+ viewTitle: "Elige tu vista",
+ viewSubtitle: "Selecciona el diseno que mejor se adapte a ti",
+ viewSimple: "Simple",
+ viewSimpleDesc: "Cuadricula semanal con franjas horarias",
+ viewCalendar: "Calendario",
+ viewCalendarDesc: "Vista diaria estilo Google Calendar",
+ viewList: "Lista",
+ viewListDesc: "Lista de tareas sin cuadricula horaria",
+ viewKanban: "Kanban",
+ viewKanbanDesc: "Tablero arrastrar y soltar con columnas",
+ scheduleTitle: "Tu horario de trabajo",
+ scheduleSubtitle: "Define cuando empieza y termina tu dia",
+ startHour: "Inicio",
+ endHour: "Fin",
+ viewDays: "Dias por semana",
+ styleTitle: "Hazlo tuyo",
+ styleSubtitle: "Personaliza fuentes y apariencia",
+ headlineFont: "Fuente de titulo",
+ bodyFont: "Fuente de texto",
+ fontSize: "Tamano de fuente",
+ darkMode: "Modo oscuro",
+ },
+};
+
+const COMMON_TIMEZONES = [
+ "Europe/Berlin", "Europe/London", "Europe/Paris", "Europe/Madrid",
+ "Europe/Rome", "Europe/Amsterdam", "Europe/Zurich", "Europe/Vienna",
+ "America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles",
+ "America/Toronto", "America/Sao_Paulo", "America/Mexico_City",
+ "Asia/Tokyo", "Asia/Shanghai", "Asia/Kolkata", "Asia/Singapore", "Asia/Dubai",
+ "Australia/Sydney", "Pacific/Auckland", "UTC"
+];
+
+const LANGUAGES = [
+ { code: "de", label: "Deutsch", flag: "DE" },
+ { code: "en", label: "English", flag: "EN" },
+ { code: "fr", label: "Francais", flag: "FR" },
+ { code: "es", label: "Espanol", flag: "ES" },
+];
+
+export default function OnboardingWizard({
+ profile,
+ darkMode,
+ language,
+ connections,
+ onComplete,
+ onSkip,
+ saveSetting,
+ onLanguageChange,
+ onDarkModeToggle,
+}: OnboardingWizardProps) {
+ const [currentStep, setCurrentStep] = useState(0);
+ const [direction, setDirection] = useState<"forward" | "backward">("forward");
+ const [animKey, setAnimKey] = useState(0);
+
+ // Local state for settings
+ const [selectedLang, setSelectedLang] = useState(language || "de");
+ const [selectedTimezone, setSelectedTimezone] = useState(profile.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC");
+ const [selectedView, setSelectedView] = useState(profile.viewStyle || "simple");
+ const [selectedStartHour, setSelectedStartHour] = useState(profile.startHour ?? 8);
+ const [selectedEndHour, setSelectedEndHour] = useState(profile.endHour ?? 18);
+ const [selectedViewDays, setSelectedViewDays] = useState(profile.viewDays ?? 7);
+ const [selectedHeadlineFont, setSelectedHeadlineFont] = useState(profile.headlineFont || "Oswald");
+ const [selectedBodyFont, setSelectedBodyFont] = useState(profile.bodyFont || "Inter");
+ const [selectedFontSize, setSelectedFontSize] = useState(profile.fontSize || "M");
+
+ // Resume from localStorage if returning from OAuth
+ useEffect(() => {
+ const savedStep = localStorage.getItem("onboarding_step");
+ if (savedStep) {
+ setCurrentStep(parseInt(savedStep, 10));
+ localStorage.removeItem("onboarding_step");
+ }
+ }, []);
+
+ const t = translations[selectedLang] || translations.en;
+
+ const goNext = async () => {
+ await saveCurrentStep();
+ setDirection("forward");
+ setAnimKey((k) => k + 1);
+ setCurrentStep((s) => Math.min(s + 1, STEPS.length - 1));
+ };
+
+ const goBack = () => {
+ setDirection("backward");
+ setAnimKey((k) => k + 1);
+ setCurrentStep((s) => Math.max(s - 1, 0));
+ };
+
+ const saveCurrentStep = async () => {
+ const step = STEPS[currentStep];
+ if (step === "language") {
+ await saveSetting("language", selectedLang);
+ await saveSetting("timezone", selectedTimezone);
+ onLanguageChange(selectedLang);
+ } else if (step === "view") {
+ await saveSetting("viewStyle", selectedView);
+ await saveSetting("showTimeGrid", selectedView === "simple" || selectedView === "calendar");
+ } else if (step === "schedule") {
+ await saveSetting("startHour", selectedStartHour);
+ await saveSetting("endHour", selectedEndHour);
+ await saveSetting("viewDays", selectedViewDays);
+ } else if (step === "style") {
+ await saveSetting("headlineFont", selectedHeadlineFont);
+ await saveSetting("bodyFont", selectedBodyFont);
+ await saveSetting("fontSize", selectedFontSize);
+ }
+ };
+
+ const handleFinish = async () => {
+ await saveCurrentStep();
+ onComplete();
+ };
+
+ const connectedProviders = new Set(connections.map((c: any) => c.provider));
+
+ const handleOAuthConnect = (url: string) => {
+ localStorage.setItem("onboarding_step", String(currentStep));
+ window.location.href = url;
+ };
+
+ const bg = darkMode ? "#1f2937" : "#ffffff";
+ const textColor = darkMode ? "#e5e7eb" : "#333333";
+ const mutedColor = darkMode ? "#9ca3af" : "#6b7280";
+ const borderColor = darkMode ? "#374151" : "#e5e7eb";
+ const accentColor = "#0d9488";
+ const cardBg = darkMode ? "#111827" : "#f9fafb";
+
+ const btnStyle: React.CSSProperties = {
+ display: "inline-flex", alignItems: "center", gap: "6px",
+ padding: "10px 24px", borderRadius: "10px", border: "none",
+ fontSize: "0.95rem", fontWeight: 600, cursor: "pointer",
+ transition: "all 0.15s",
+ };
+ const primaryBtn: React.CSSProperties = { ...btnStyle, background: accentColor, color: "#fff" };
+ const secondaryBtn: React.CSSProperties = { ...btnStyle, background: darkMode ? "#374151" : "#f3f4f6", color: textColor };
+
+ const isLastStep = currentStep === STEPS.length - 1;
+
+ const renderStep = () => {
+ const step = STEPS[currentStep];
+
+ if (step === "welcome") {
+ return (
+
+
✓
+
+ {t.welcomeTitle}
+
+
+ {t.welcomeSubtitle}
+
+
+
+
+
+
+ );
+ }
+
+ if (step === "language") {
+ return (
+
+
{t.langTitle}
+
{t.langSubtitle}
+
+
+ {LANGUAGES.map((lang) => (
+
+ ))}
+
+
+
+
+
+ );
+ }
+
+ if (step === "connect") {
+ const providers = [
+ { key: "google", label: t.connectGoogle, icon: faGoogle, color: "#4285F4", action: () => handleOAuthConnect("/api/calendar/google/start") },
+ { key: "apple", label: t.connectApple, icon: faApple, color: darkMode ? "#ccc" : "#333", action: () => {} },
+ { key: "outlook", label: t.connectOutlook, icon: faMicrosoft, color: "#0078D4", action: () => {} },
+ { key: "synology", label: t.connectSynology, icon: faServer, color: "#007AFF", action: () => {} },
+ ];
+ return (
+
+
{t.connectTitle}
+
{t.connectSubtitle}
+
+
+ {providers.map((p) => {
+ const isConnected = connectedProviders.has(p.key);
+ return (
+
+
+ {p.label}
+ {isConnected ? (
+
+ {t.connected}
+
+ ) : (
+
+ )}
+
+ );
+ })}
+
+
{t.skipConnect}
+
+ );
+ }
+
+ if (step === "view") {
+ const views = [
+ { key: "simple", label: t.viewSimple, desc: t.viewSimpleDesc, Icon: LayoutGrid },
+ { key: "calendar", label: t.viewCalendar, desc: t.viewCalendarDesc, Icon: CalendarDays },
+ { key: "list", label: t.viewList, desc: t.viewListDesc, Icon: ListTodo },
+ { key: "kanban", label: t.viewKanban, desc: t.viewKanbanDesc, Icon: Kanban },
+ ];
+ return (
+
+
{t.viewTitle}
+
{t.viewSubtitle}
+
+
+ {views.map((v) => {
+ const selected = selectedView === v.key;
+ return (
+
+ );
+ })}
+
+
+ );
+ }
+
+ if (step === "schedule") {
+ return (
+
+
{t.scheduleTitle}
+
{t.scheduleSubtitle}
+
+
+
+
+
+ setSelectedStartHour(parseInt(e.target.value))}
+ style={{ flex: 1, accentColor }}
+ />
+
+ {String(selectedStartHour).padStart(2, "0")}:00
+
+
+
+
+
+
+ setSelectedEndHour(parseInt(e.target.value))}
+ style={{ flex: 1, accentColor }}
+ />
+
+ {String(selectedEndHour).padStart(2, "0")}:00
+
+
+
+
+
+
+
+ {[5, 7].map((d) => (
+
+ ))}
+
+
+ );
+ }
+
+ if (step === "style") {
+ return (
+
+
{t.styleTitle}
+
{t.styleSubtitle}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(["S", "M", "L"] as const).map((size) => (
+
+ ))}
+
+
+
+
+
{t.darkMode}
+
+
+
+ );
+ }
+
+ return null;
+ };
+
+ return (
+
+
+ {/* Close button */}
+ {currentStep > 0 && (
+
+ )}
+
+ {/* Step content with animation */}
+
+ {renderStep()}
+
+
+ {/* Footer with progress + nav (not on welcome) */}
+ {currentStep > 0 && (
+
+
+
+ {/* Progress dots */}
+
+ {STEPS.map((_, i) => (
+
+ ))}
+
+
+ {isLastStep ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx
index c928554..d4dd365 100644
--- a/src/components/WeeklyView.tsx
+++ b/src/components/WeeklyView.tsx
@@ -88,6 +88,7 @@ import SimpleDatePicker from "./SimpleDatePicker";
import RecurringTasksManager from "./RecurringTasksManager";
export interface RecurringTaskException { id: string; taskId: string; originalDate: string; newDate?: string | null; isCancelled: boolean; createdAt: Date; updatedAt: Date; }
import { ImportListModal } from "./ImportListModal";
+import OnboardingWizard from "./OnboardingWizard";
import { getRandomLocalQuote } from "@/lib/quotes";
// Cookie helpers for per-device settings
@@ -1822,6 +1823,7 @@ export default function WeeklyView() {
// Moved state definitions to the top
const [showSettings, setShowSettings] = useState(false);
+ const [showOnboarding, setShowOnboarding] = useState(false);
const [activeTab, setActiveTab] = useState<
"calendar" | "general" | "account" | "styling" | "motivation" | "about"
>("general");
@@ -2236,6 +2238,11 @@ export default function WeeklyView() {
if (profileData.showSubHourSlots !== undefined) setShowSubHourSlots(profileData.showSubHourSlots);
if (profileData.allDayPosition) setAllDayPosition(profileData.allDayPosition);
if (profileData.viewSettings) setViewSettings(profileData.viewSettings);
+
+ // Show onboarding wizard for new users
+ if (profileData.hasCompletedOnboarding === false) {
+ setShowOnboarding(true);
+ }
}
}
} catch (err) {
@@ -6463,8 +6470,11 @@ export default function WeeklyView() {
draggable
onDragStart={(e) => {
e.dataTransfer.setData("text/kanban-task", task.id);
+ e.dataTransfer.setData("text/plain", task.id);
e.dataTransfer.effectAllowed = "move";
+ setDraggedTask(task);
}}
+ onDragEnd={() => setDraggedTask(null)}
onClick={(e) => {
const target = e.target as HTMLElement;
if (target.tagName === "INPUT" || target.contentEditable === "true" || target.closest("button") || target.closest(".kanban-card-subtask-list")) return;
@@ -7280,6 +7290,7 @@ export default function WeeklyView() {
projects={projects}
onProjectAssign={assignProject}
kanbanStages={kanbanStages}
+ weatherEnabled={effectiveWeatherEnabled}
/>
))}
{visibleSlots.map((slot) => {
@@ -7369,7 +7380,7 @@ export default function WeeklyView() {
if (d.includes("humidity") && w.humidity != null) parts.push(`💧${w.humidity}%`);
if (d.includes("uv") && w.uv != null && w.uv > 0) parts.push(`UV${w.uv}`);
return (
-
+
{d.includes("icon") && {getWeatherIcon(w.code)}}
{parts.length > 0 && {parts.join(" ")}}
@@ -8879,6 +8890,21 @@ export default function WeeklyView() {
)
}
+ {/* Onboarding Wizard */}
+ {showOnboarding && (
+
{ saveSetting("hasCompletedOnboarding", true); setShowOnboarding(false); }}
+ onSkip={() => { saveSetting("hasCompletedOnboarding", true); setShowOnboarding(false); }}
+ saveSetting={saveSetting}
+ onLanguageChange={(lang: string) => { setLanguage(lang); }}
+ onDarkModeToggle={() => setDarkMode(!darkMode)}
+ />
+ )}
+
{
selectedTaskForNotes && (
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index ba76f28..a10bcb0 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -136,5 +136,16 @@ export const authOptions: NextAuthOptions = {
return session;
}
},
+ events: {
+ async createUser({ user }) {
+ // New OAuth users should see the onboarding wizard
+ if (user.id) {
+ await prisma.user.update({
+ where: { id: user.id },
+ data: { hasCompletedOnboarding: false },
+ });
+ }
+ },
+ },
secret: process.env.NEXTAUTH_SECRET
};
\ No newline at end of file