7.5 Full Code Reference
Below is the complete, runnable code for the "AI Daily Focus Helper." You can copy and paste it directly.
Project Structure
daily-focus/
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ └── motivate/
│ │ │ └── route.ts # AI encouragement API
│ │ ├── layout.tsx # Global layout
│ │ ├── page.tsx # Main page
│ │ └── globals.css # Global styles
│ ├── components/
│ │ ├── TaskInput.tsx # Task input component
│ │ ├── MotivationCard.tsx # AI encouragement card
│ │ ├── Stats.tsx # Statistics component
│ │ └── ThemeToggle.tsx # Dark mode toggle
│ └── lib/
│ ├── supabase.ts # Supabase client
│ └── user.ts # Anonymous user management
├── .env.local # Environment variables (don't commit to Git)
├── tailwind.config.ts # Tailwind config
├── package.json
└── README.md
Environment Setup
1. Create .env.local
# Supabase - get from supabase.com Settings > API
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# OpenAI - create at platform.openai.com/api-keys
OPENAI_API_KEY=sk-your-key
2. Install Dependencies
npm install @supabase/supabase-js openai
3. Database Schema
Run this in the Supabase console's SQL Editor:
-- Create the tasks table
CREATE TABLE IF NOT EXISTS tasks (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id TEXT NOT NULL,
content TEXT NOT NULL,
is_completed BOOLEAN DEFAULT FALSE,
position INTEGER NOT NULL CHECK (position IN (0, 1, 2)),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Index for querying by user and date
CREATE INDEX IF NOT EXISTS idx_tasks_user_date
ON tasks(user_id, created_at DESC);
-- Disable RLS for development (remember to enable and configure before going live)
ALTER TABLE tasks DISABLE ROW LEVEL SECURITY;
Core Code
src/lib/supabase.ts
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
// Create a Supabase client instance
// This instance is shared across the entire application
export const supabase = createClient(supabaseUrl, supabaseKey);
src/lib/user.ts
// Manage anonymous user IDs
// Uses localStorage for persistence, ensuring the same browser always uses the same ID
const USER_ID_KEY = "daily-focus-user-id";
export function getUserId(): string {
if (typeof window === "undefined") return "";
let userId = localStorage.getItem(USER_ID_KEY);
if (!userId) {
// Generate a random anonymous ID
userId = "anon-" + Math.random().toString(36).substring(2, 15);
localStorage.setItem(USER_ID_KEY, userId);
}
return userId;
}
// Get today's date string for database queries
export function getTodayString(): string {
return new Date().toISOString().split("T")[0]; // "2024-01-15"
}
src/app/api/motivate/route.ts
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
// Initialize OpenAI client
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export async function POST(request: NextRequest) {
try {
// 1. Get the user's three tasks from the request body
const { tasks } = await request.json();
if (!tasks || tasks.length !== 3) {
return NextResponse.json(
{ error: "Please provide three tasks" },
{ status: 400 }
);
}
// 2. Filter out empty tasks
const validTasks = tasks.filter((t: string) => t.trim() !== "");
if (validTasks.length === 0) {
return NextResponse.json(
{ error: "Please fill in at least one task" },
{ status: 400 }
);
}
// 3. Call OpenAI API to generate encouragement
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini", // Affordable and sufficient
messages: [
{
role: "system",
content: `You are a warm, positive encouragement coach.
The user has set the following tasks for today. You need to:
1. Write a short encouragement message (under 50 words) — warm but not cheesy
2. Give a practical tip to help the user efficiently complete today's tasks (under 50 words)
Return in JSON format: {"message": "encouragement", "tip": "tip"}`,
},
{
role: "user",
content: `My tasks for today are: ${validTasks.join(", ")}`,
},
],
temperature: 0.8, // Slightly higher for more interesting responses
max_tokens: 300,
});
// 4. Parse the AI's response
const content = completion.choices[0].message.content || "";
let result;
try {
result = JSON.parse(content);
} catch {
// Fallback if AI returns non-standard JSON
result = {
message: "Keep going! Every small task completed is a step closer to your goal 💪",
tip: "Start with the easiest task — give yourself a small reward after each one 🎁",
};
}
return NextResponse.json(result);
} catch (error) {
console.error("AI motivate error:", error);
return NextResponse.json(
{ error: "Failed to generate encouragement, please try again later" },
{ status: 500 }
);
}
}
src/components/TaskInput.tsx
"use client";
interface TaskInputProps {
index: number; // Task index (0, 1, 2)
value: string; // Task content
isCompleted: boolean; // Whether completed
onChange: (value: string) => void;
onToggle: () => void;
}
export default function TaskInput({
index,
value,
isCompleted,
onChange,
onToggle,
}: TaskInputProps) {
return (
<div className="flex items-center gap-3 group">
{/* Checkbox */}
<button
onClick={onToggle}
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center
transition-all duration-200 flex-shrink-0
${
isCompleted
? "bg-orange-500 border-orange-500 scale-110"
: "border-gray-300 dark:border-gray-600 hover:border-orange-400"
}`}
>
{isCompleted && (
<svg className="w-3.5 h-3.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
{/* Input field */}
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={`Most important thing today #${index + 1}`}
className={`flex-1 px-4 py-3 rounded-xl border transition-all duration-200
bg-white dark:bg-gray-800
border-gray-200 dark:border-gray-700
focus:outline-none focus:ring-2 focus:ring-orange-300 focus:border-transparent
placeholder:text-gray-400 dark:placeholder:text-gray-500
text-gray-800 dark:text-gray-100
${isCompleted ? "line-through text-gray-400 dark:text-gray-500" : ""}`}
/>
</div>
);
}
src/components/MotivationCard.tsx
"use client";
interface MotivationCardProps {
message: string;
tip: string;
}
export default function MotivationCard({ message, tip }: MotivationCardProps) {
return (
<div className="mt-6 p-5 rounded-2xl bg-gradient-to-br from-orange-50 to-amber-50
dark:from-orange-900/20 dark:to-amber-900/20
border border-orange-100 dark:border-orange-800/30
animate-fade-in">
{/* Encouragement */}
<p className="text-gray-800 dark:text-gray-100 text-base leading-relaxed">
💬 {message}
</p>
{/* Divider */}
<div className="my-3 border-t border-orange-100 dark:border-orange-800/30" />
{/* Practical tip */}
<p className="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
💡 {tip}
</p>
</div>
);
}
src/components/Stats.tsx
"use client";
interface StatsProps {
completedCount: number; // Today's completed count
totalCount: number; // Today's total tasks
streak: number; // Consecutive completion days
}
export default function Stats({ completedCount, totalCount, streak }: StatsProps) {
const allDone = completedCount === totalCount && totalCount > 0;
return (
<div className="flex items-center justify-between mb-8">
{/* Completion progress */}
<div className="flex items-center gap-2">
<span className="text-2xl font-bold text-orange-500">
{completedCount}/{totalCount}
</span>
<span className="text-sm text-gray-500 dark:text-gray-400">completed</span>
{allDone && <span className="text-2xl animate-bounce">🎉</span>}
</div>
{/* Streak days */}
{streak > 0 && (
<div className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
<span>🔥</span>
<span><strong className="text-orange-500">{streak}</strong> day streak</span>
</div>
)}
</div>
);
}
src/components/ThemeToggle.tsx
"use client";
import { useState, useEffect } from "react";
export default function ThemeToggle() {
const [isDark, setIsDark] = useState(false);
// Read preference from localStorage, or follow system setting
useEffect(() => {
const saved = localStorage.getItem("theme");
if (saved === "dark" || (!saved && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
setIsDark(true);
document.documentElement.classList.add("dark");
}
}, []);
const toggle = () => {
const next = !isDark;
setIsDark(next);
document.documentElement.classList.toggle("dark");
localStorage.setItem("theme", next ? "dark" : "light");
};
return (
<button
onClick={toggle}
className="p-2 rounded-lg text-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition"
title={isDark ? "Switch to light mode" : "Switch to dark mode"}
>
{isDark ? "☀️" : "🌙"}
</button>
);
}
src/app/page.tsx (Main Page)
"use client";
import { useState, useEffect } from "react";
import { supabase } from "@/lib/supabase";
import { getUserId, getTodayString } from "@/lib/user";
import TaskInput from "@/components/TaskInput";
import MotivationCard from "@/components/MotivationCard";
import Stats from "@/components/Stats";
import ThemeToggle from "@/components/ThemeToggle";
interface Motivation {
message: string;
tip: string;
}
export default function Home() {
// Task content state
const [tasks, setTasks] = useState<string[]>(["", "", ""]);
// Task completion state
const [completed, setCompleted] = useState<boolean[]>([false, false, false]);
// AI encouragement content
const [motivation, setMotivation] = useState<Motivation | null>(null);
// Various loading states
const [saving, setSaving] = useState(false);
const [loadingMotivation, setLoadingMotivation] = useState(false);
const [saved, setSaved] = useState(false);
// Streak days
const [streak, setStreak] = useState(0);
// Initial page load
const [initialLoading, setInitialLoading] = useState(true);
// Load today's tasks from database on page load
useEffect(() => {
async function loadTodayTasks() {
const userId = getUserId();
const today = getTodayString();
const { data, error } = await supabase
.from("tasks")
.select("*")
.eq("user_id", userId)
.gte("created_at", `${today}T00:00:00`)
.lt("created_at", `${today}T23:59:59`)
.order("position");
if (data && data.length > 0) {
const loadedTasks = ["", "", ""];
const loadedCompleted = [false, false, false];
data.forEach((row) => {
loadedTasks[row.position] = row.content;
loadedCompleted[row.position] = row.is_completed;
});
setTasks(loadedTasks);
setCompleted(loadedCompleted);
}
setInitialLoading(false);
}
loadTodayTasks();
}, []);
// Save tasks to database
const saveTasks = async () => {
const userId = getUserId();
setSaving(true);
setSaved(false);
// First delete today's old data
const today = getTodayString();
await supabase
.from("tasks")
.delete()
.eq("user_id", userId)
.gte("created_at", `${today}T00:00:00`)
.lt("created_at", `${today}T23:59:59`);
// Insert new data (only save non-empty tasks)
const rows = tasks
.map((content, index) => ({
user_id: userId,
content,
is_completed: completed[index],
position: index,
}))
.filter((row) => row.content.trim() !== "");
if (rows.length > 0) {
await supabase.from("tasks").insert(rows);
}
setSaving(false);
setSaved(true);
setTimeout(() => setSaved(false), 2000); // Hide "saved" after 2 seconds
};
// Toggle completion state
const toggleComplete = async (index: number) => {
const newCompleted = [...completed];
newCompleted[index] = !newCompleted[index];
setCompleted(newCompleted);
// Sync to database in real-time
const userId = getUserId();
const today = getTodayString();
await supabase
.from("tasks")
.update({ is_completed: newCompleted[index] })
.eq("user_id", userId)
.eq("position", index)
.gte("created_at", `${today}T00:00:00`)
.lt("created_at", `${today}T23:59:59`);
};
// Get AI encouragement
const getMotivation = async () => {
setLoadingMotivation(true);
setMotivation(null);
try {
const res = await fetch("/api/motivate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tasks }),
});
const data = await res.json();
if (res.ok) {
setMotivation(data);
} else {
setMotivation({
message: "Keep going! Every small task completed is progress 💪",
tip: "Start with the easiest task 🚀",
});
}
} catch {
setMotivation({
message: "Keep going! Every small task completed is progress 💪",
tip: "Start with the easiest task 🚀",
});
}
setLoadingMotivation(false);
};
// Calculate completion count
const completedCount = completed.filter(Boolean).length;
const hasAnyTask = tasks.some((t) => t.trim() !== "");
// Skeleton screen
if (initialLoading) {
return (
<main className="min-h-screen flex items-center justify-center bg-gradient-to-b from-orange-50 to-white dark:from-gray-900 dark:to-gray-950">
<div className="max-w-md w-full px-6 py-10 space-y-4">
<div className="h-8 bg-gray-200 dark:bg-gray-800 rounded-lg animate-pulse" />
<div className="h-4 bg-gray-200 dark:bg-gray-800 rounded-lg animate-pulse w-2/3 mx-auto" />
<div className="h-14 bg-gray-200 dark:bg-gray-800 rounded-xl animate-pulse mt-8" />
<div className="h-14 bg-gray-200 dark:bg-gray-800 rounded-xl animate-pulse" />
<div className="h-14 bg-gray-200 dark:bg-gray-800 rounded-xl animate-pulse" />
<div className="h-12 bg-gray-200 dark:bg-gray-800 rounded-xl animate-pulse mt-4" />
</div>
</main>
);
}
return (
<main className="min-h-screen bg-gradient-to-b from-orange-50 to-white dark:from-gray-900 dark:to-gray-950 transition-colors">
{/* Top-right: dark mode toggle */}
<div className="absolute top-4 right-4">
<ThemeToggle />
</div>
<div className="max-w-md mx-auto px-6 py-12">
{/* Title */}
<h1 className="text-3xl font-bold text-center text-orange-600 dark:text-orange-400">
🎯 AI Daily Focus Helper
</h1>
<p className="text-center text-gray-500 dark:text-gray-400 mt-2 mb-8">
Focus on the 3 most important things every day
</p>
{/* Stats area */}
<Stats completedCount={completedCount} totalCount={3} streak={streak} />
{/* Task input area */}
<div className="space-y-3">
{tasks.map((task, index) => (
<TaskInput
key={index}
index={index}
value={task}
isCompleted={completed[index]}
onChange={(value) => {
const newTasks = [...tasks];
newTasks[index] = value;
setTasks(newTasks);
}}
onToggle={() => toggleComplete(index)}
/>
))}
</div>
{/* Save button */}
<button
onClick={saveTasks}
disabled={saving || !hasAnyTask}
className="w-full mt-6 py-3 rounded-xl font-medium transition-all duration-200
bg-orange-500 hover:bg-orange-600 active:scale-[0.98]
text-white disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? "Saving..." : saved ? "Saved ✓" : "Save Today's Focus"}
</button>
{/* AI encouragement button */}
<button
onClick={getMotivation}
disabled={loadingMotivation || !hasAnyTask}
className="w-full mt-3 py-3 rounded-xl font-medium transition-all duration-200
bg-white dark:bg-gray-800 border border-orange-200 dark:border-orange-800/30
text-orange-600 dark:text-orange-400
hover:bg-orange-50 dark:hover:bg-gray-700
disabled:opacity-50 disabled:cursor-not-allowed"
>
{loadingMotivation ? (
<span className="flex items-center justify-center gap-2">
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Thinking...
</span>
) : (
"✨ Get Today's Encouragement"
)}
</button>
{/* AI encouragement content */}
{motivation && <MotivationCard message={motivation.message} tip={motivation.tip} />}
{/* Empty state guidance */}
{!hasAnyTask && (
<div className="mt-8 text-center">
<p className="text-4xl mb-3">📝 🌅 ✨</p>
<p className="text-gray-400 dark:text-gray-500 text-sm">
A new day begins! Set your 3 most important things for today
</p>
</div>
)}
</div>
</main>
);
}
src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "AI Daily Focus Helper",
description: "Focus on the 3 most important things every day, get AI encouragement",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body className={inter.className}>{children}</body>
</html>
);
}
tailwind.config.ts
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
darkMode: "class", // Use class to toggle dark mode
theme: {
extend: {
animation: {
"fade-in": "fadeIn 0.3s ease-out",
},
keyframes: {
fadeIn: {
"0%": { opacity: "0", transform: "translateY(10px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
},
},
},
plugins: [],
};
export default config;
FAQ
Q: Startup error "NEXT_PUBLIC_SUPABASE_URL is undefined"
A: Check that your .env.local file is in the project root (same level as package.json). After modifying environment variables, restart the dev server (Ctrl+C then npm run dev).
Q: Supabase save returns 403 error
A: Check if Row Level Security is disabled. Run in Supabase SQL Editor:
ALTER TABLE tasks DISABLE ROW LEVEL SECURITY;
Q: OpenAI API returns 401 error
A: There's an issue with the API Key. Check:
- No extra spaces or quotes in
.env.local - The key starts with
sk- - Check your account balance in the OpenAI dashboard
Q: OpenAI API returns 429 error
A: Too many requests. This is fine during development — wait a few seconds and try again. If it persists, check whether the API is being called in a loop.
Q: Page layout is broken on mobile
A: Check if layout.tsx has the viewport set:
export const viewport = {
width: "device-width",
initialScale: 1,
};
Q: Dark mode doesn't work
A: Make sure tailwind.config.ts has darkMode: "class" configured, and that the ThemeToggle component correctly toggles the dark class on the <html> tag.
Q: Vercel deployment succeeded but page is blank
A: The most common cause is missing environment variables. Go to Vercel project Settings → Environment Variables and check that all three variables are configured. After configuring, redeploy (Deployments → latest → Redeploy).
Q: How do I see what's in the database?
A: Supabase console → Table Editor → select the tasks table. You'll see all the data and can even edit it directly.
Q: Code changed but browser didn't update?
A: Try these:
- Hard refresh: Ctrl+Shift+R (Mac: Cmd+Shift+R)
- Clear cache: DevTools → Network → check "Disable cache"
- Restart the dev server
This is the complete code. Copy these files, configure your environment variables, and you can run a fully functional "AI Daily Focus Helper."
If you want to extend functionality, you can add:
- History page
- Weekly statistics charts
- WeChat login
- Daily reminder push notifications
These are left as homework for you 🚀