This commit is contained in:
@@ -4,16 +4,17 @@ import { generateMemorableToken, isValidToken } from '@/lib/auth/tokens';
|
|||||||
import { setTokenCookie, getTokenCookie } from '@/lib/auth/cookies';
|
import { setTokenCookie, getTokenCookie } from '@/lib/auth/cookies';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
// Check if user already has a token
|
// Check if user already has a token
|
||||||
const existingToken = await getTokenCookie();
|
const existingToken = await getTokenCookie();
|
||||||
|
|
||||||
if (existingToken) {
|
if (existingToken) {
|
||||||
// Verify token exists in database
|
// Verify token exists in database
|
||||||
const [user] = await db.select().from(users).where(eq(users.token, existingToken));
|
const userRows = await db.select().from(users).where(eq(users.token, existingToken));
|
||||||
|
|
||||||
if (user) {
|
if (userRows.length > 0) {
|
||||||
|
const user = userRows[0];
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
token: existingToken,
|
token: existingToken,
|
||||||
@@ -31,20 +32,25 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = (await request.json()) as { action: string; token?: string };
|
||||||
const { action, token } = body;
|
const { action, token } = body;
|
||||||
|
|
||||||
if (action === 'create') {
|
if (action === 'create') {
|
||||||
// Generate new token and create user
|
// Generate new token and create user
|
||||||
const newToken = generateMemorableToken();
|
const newToken = generateMemorableToken();
|
||||||
|
|
||||||
const [newUser] = await db
|
const newUserRows = await db
|
||||||
.insert(users)
|
.insert(users)
|
||||||
.values({
|
.values({
|
||||||
token: newToken,
|
token: newToken,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
if (newUserRows.length === 0) {
|
||||||
|
throw new Error('Failed to create user');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = newUserRows[0];
|
||||||
await setTokenCookie(newToken);
|
await setTokenCookie(newToken);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -67,9 +73,9 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if token exists
|
// Check if token exists
|
||||||
const [user] = await db.select().from(users).where(eq(users.token, token));
|
const userRows = await db.select().from(users).where(eq(users.token, token));
|
||||||
|
|
||||||
if (!user) {
|
if (userRows.length === 0) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
@@ -79,6 +85,7 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = userRows[0];
|
||||||
await setTokenCookie(token);
|
await setTokenCookie(token);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
@@ -7,8 +7,8 @@ async function getUserFromToken() {
|
|||||||
const token = await getTokenCookie();
|
const token = await getTokenCookie();
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
|
|
||||||
const [user] = await db.select().from(users).where(eq(users.token, token));
|
const userRows = await db.select().from(users).where(eq(users.token, token));
|
||||||
return user;
|
return userRows.length > 0 ? userRows[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
@@ -26,20 +26,20 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify habit belongs to user
|
// Verify habit belongs to user
|
||||||
const [habit] = await db
|
const habitRows = await db
|
||||||
.select()
|
.select()
|
||||||
.from(habits)
|
.from(habits)
|
||||||
.where(and(eq(habits.id, habitId), eq(habits.userId, user.id)));
|
.where(and(eq(habits.id, habitId), eq(habits.userId, user.id)));
|
||||||
|
|
||||||
if (!habit) {
|
if (habitRows.length === 0) {
|
||||||
return NextResponse.json({ error: 'Habit not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Habit not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = (await request.json()) as { note?: string };
|
||||||
const { note } = body;
|
const { note } = body;
|
||||||
|
|
||||||
// Create log entry
|
// Create log entry
|
||||||
const [log] = await db
|
const logRows = await db
|
||||||
.insert(habitLogs)
|
.insert(habitLogs)
|
||||||
.values({
|
.values({
|
||||||
habitId,
|
habitId,
|
||||||
@@ -47,6 +47,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
if (logRows.length === 0) {
|
||||||
|
throw new Error('Failed to create log entry');
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = logRows[0];
|
||||||
return NextResponse.json({ log });
|
return NextResponse.json({ log });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Log habit error:', error);
|
console.error('Log habit error:', error);
|
||||||
|
@@ -7,11 +7,11 @@ async function getUserFromToken() {
|
|||||||
const token = await getTokenCookie();
|
const token = await getTokenCookie();
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
|
|
||||||
const [user] = await db.select().from(users).where(eq(users.token, token));
|
const userRows = await db.select().from(users).where(eq(users.token, token));
|
||||||
return user;
|
return userRows.length > 0 ? userRows[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const user = await getUserFromToken();
|
const user = await getUserFromToken();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -83,7 +83,13 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = (await request.json()) as {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
targetFrequency?: { value: number; period: 'day' | 'week' | 'month' };
|
||||||
|
color?: string;
|
||||||
|
icon?: string;
|
||||||
|
};
|
||||||
const { name, type, targetFrequency, color, icon } = body;
|
const { name, type, targetFrequency, color, icon } = body;
|
||||||
|
|
||||||
if (!name || !type) {
|
if (!name || !type) {
|
||||||
@@ -95,18 +101,33 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [newHabit] = await db
|
// Validate type is one of the allowed enum values
|
||||||
|
if (!['positive', 'neutral', 'negative'].includes(type)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'Type must be one of: positive, neutral, negative',
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newHabitRows = await db
|
||||||
.insert(habits)
|
.insert(habits)
|
||||||
.values({
|
.values({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name,
|
name,
|
||||||
type,
|
type: type as 'positive' | 'neutral' | 'negative',
|
||||||
targetFrequency,
|
targetFrequency,
|
||||||
color,
|
color,
|
||||||
icon,
|
icon,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
if (newHabitRows.length === 0) {
|
||||||
|
throw new Error('Failed to create habit');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newHabit = newHabitRows[0];
|
||||||
return NextResponse.json({ habit: newHabit });
|
return NextResponse.json({ habit: newHabit });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Create habit error:', error);
|
console.error('Create habit error:', error);
|
||||||
|
@@ -36,6 +36,12 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
|
interface AuthData {
|
||||||
|
authenticated: boolean;
|
||||||
|
token?: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Habit {
|
interface Habit {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -47,6 +53,23 @@ interface Habit {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HabitsResponse {
|
||||||
|
habits: Habit[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogResponse {
|
||||||
|
log: {
|
||||||
|
id: number;
|
||||||
|
habitId: number;
|
||||||
|
loggedAt: string;
|
||||||
|
note?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HabitResponse {
|
||||||
|
habit: Habit;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -57,11 +80,11 @@ export default function Dashboard() {
|
|||||||
const [copiedToken, setCopiedToken] = useState(false);
|
const [copiedToken, setCopiedToken] = useState(false);
|
||||||
|
|
||||||
// Check authentication
|
// Check authentication
|
||||||
const { data: authData, isLoading: authLoading } = useQuery({
|
const { data: authData, isLoading: authLoading } = useQuery<AuthData>({
|
||||||
queryKey: ['auth'],
|
queryKey: ['auth'],
|
||||||
queryFn: async () => {
|
queryFn: async (): Promise<AuthData> => {
|
||||||
const res = await fetch('/api/auth');
|
const res = await fetch('/api/auth');
|
||||||
const data = await res.json();
|
const data = (await res.json()) as AuthData;
|
||||||
if (!data.authenticated) {
|
if (!data.authenticated) {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
}
|
}
|
||||||
@@ -76,45 +99,45 @@ export default function Dashboard() {
|
|||||||
}, [authData]);
|
}, [authData]);
|
||||||
|
|
||||||
// Fetch habits
|
// Fetch habits
|
||||||
const { data: habitsData, isLoading: habitsLoading } = useQuery({
|
const { data: habitsData, isLoading: habitsLoading } = useQuery<HabitsResponse>({
|
||||||
queryKey: ['habits'],
|
queryKey: ['habits'],
|
||||||
queryFn: async () => {
|
queryFn: async (): Promise<HabitsResponse> => {
|
||||||
const res = await fetch('/api/habits');
|
const res = await fetch('/api/habits');
|
||||||
if (!res.ok) throw new Error('Failed to fetch habits');
|
if (!res.ok) throw new Error('Failed to fetch habits');
|
||||||
return res.json();
|
return res.json() as Promise<HabitsResponse>;
|
||||||
},
|
},
|
||||||
enabled: !!authData?.authenticated,
|
enabled: !!authData?.authenticated,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log habit mutation
|
// Log habit mutation
|
||||||
const logHabitMutation = useMutation({
|
const logHabitMutation = useMutation<LogResponse, Error, number>({
|
||||||
mutationFn: async (habitId: number) => {
|
mutationFn: async (habitId: number): Promise<LogResponse> => {
|
||||||
const res = await fetch(`/api/habits/${habitId}/log`, {
|
const res = await fetch(`/api/habits/${String(habitId)}/log`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({}),
|
body: JSON.stringify({}),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Failed to log habit');
|
if (!res.ok) throw new Error('Failed to log habit');
|
||||||
return res.json();
|
return res.json() as Promise<LogResponse>;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['habits'] });
|
void queryClient.invalidateQueries({ queryKey: ['habits'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create habit mutation
|
// Create habit mutation
|
||||||
const createHabitMutation = useMutation({
|
const createHabitMutation = useMutation<HabitResponse, Error, { name: string; type: string }>({
|
||||||
mutationFn: async (data: { name: string; type: string }) => {
|
mutationFn: async (data: { name: string; type: string }): Promise<HabitResponse> => {
|
||||||
const res = await fetch('/api/habits', {
|
const res = await fetch('/api/habits', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Failed to create habit');
|
if (!res.ok) throw new Error('Failed to create habit');
|
||||||
return res.json();
|
return res.json() as Promise<HabitResponse>;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['habits'] });
|
void queryClient.invalidateQueries({ queryKey: ['habits'] });
|
||||||
setShowNewHabitDialog(false);
|
setShowNewHabitDialog(false);
|
||||||
setNewHabitName('');
|
setNewHabitName('');
|
||||||
setNewHabitType('neutral');
|
setNewHabitType('neutral');
|
||||||
@@ -132,9 +155,11 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const copyToken = () => {
|
const copyToken = () => {
|
||||||
if (userToken) {
|
if (userToken) {
|
||||||
navigator.clipboard.writeText(userToken);
|
void navigator.clipboard.writeText(userToken);
|
||||||
setCopiedToken(true);
|
setCopiedToken(true);
|
||||||
setTimeout(() => setCopiedToken(false), 2000);
|
setTimeout(() => {
|
||||||
|
setCopiedToken(false);
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,7 +224,7 @@ export default function Dashboard() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const habits = habitsData?.habits || [];
|
const habits = habitsData?.habits ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black">
|
<div className="min-h-screen bg-black">
|
||||||
@@ -233,13 +258,20 @@ export default function Dashboard() {
|
|||||||
id="name"
|
id="name"
|
||||||
placeholder="e.g., Exercise, Read, Meditate..."
|
placeholder="e.g., Exercise, Read, Meditate..."
|
||||||
value={newHabitName}
|
value={newHabitName}
|
||||||
onChange={(e) => setNewHabitName(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setNewHabitName(e.target.value);
|
||||||
|
}}
|
||||||
className="border-zinc-800 bg-zinc-900"
|
className="border-zinc-800 bg-zinc-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="type">Habit Type</Label>
|
<Label htmlFor="type">Habit Type</Label>
|
||||||
<Select value={newHabitType} onValueChange={(value: any) => setNewHabitType(value)}>
|
<Select
|
||||||
|
value={newHabitType}
|
||||||
|
onValueChange={(value: 'positive' | 'neutral' | 'negative') => {
|
||||||
|
setNewHabitType(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SelectTrigger className="border-zinc-800 bg-zinc-900">
|
<SelectTrigger className="border-zinc-800 bg-zinc-900">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -267,7 +299,12 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setShowNewHabitDialog(false)}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setShowNewHabitDialog(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -317,7 +354,9 @@ export default function Dashboard() {
|
|||||||
className={`transform cursor-pointer transition-all duration-200 hover:scale-[1.02] ${getHabitCardClass(
|
className={`transform cursor-pointer transition-all duration-200 hover:scale-[1.02] ${getHabitCardClass(
|
||||||
habit.type,
|
habit.type,
|
||||||
)} ${logHabitMutation.isPending ? 'opacity-75' : ''}`}
|
)} ${logHabitMutation.isPending ? 'opacity-75' : ''}`}
|
||||||
onClick={() => logHabitMutation.mutate(habit.id)}
|
onClick={() => {
|
||||||
|
logHabitMutation.mutate(habit.id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
@@ -384,7 +423,9 @@ export default function Dashboard() {
|
|||||||
<h3 className="mb-2 text-lg font-semibold">No habits yet</h3>
|
<h3 className="mb-2 text-lg font-semibold">No habits yet</h3>
|
||||||
<p className="mb-4 text-sm text-zinc-500">Start building better habits today</p>
|
<p className="mb-4 text-sm text-zinc-500">Start building better habits today</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowNewHabitDialog(true)}
|
onClick={() => {
|
||||||
|
setShowNewHabitDialog(true);
|
||||||
|
}}
|
||||||
className="bg-emerald-600 hover:bg-emerald-700"
|
className="bg-emerald-600 hover:bg-emerald-700"
|
||||||
>
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
@@ -9,7 +9,14 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { LogIn, Sparkles, Shield, Zap, ArrowLeft } from 'lucide-react';
|
import { LogIn, Shield, Zap, ArrowLeft, Activity, CalendarCheck } from 'lucide-react';
|
||||||
|
|
||||||
|
interface AuthResponse {
|
||||||
|
success?: boolean;
|
||||||
|
token?: string;
|
||||||
|
userId?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Welcome() {
|
export default function Welcome() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -18,14 +25,14 @@ export default function Welcome() {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
const createAccountMutation = useMutation({
|
const createAccountMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async (): Promise<AuthResponse> => {
|
||||||
const res = await fetch('/api/auth', {
|
const res = await fetch('/api/auth', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ action: 'create' }),
|
body: JSON.stringify({ action: 'create' }),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Failed to create account');
|
if (!res.ok) throw new Error('Failed to create account');
|
||||||
return res.json();
|
return res.json() as Promise<AuthResponse>;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
@@ -36,17 +43,17 @@ export default function Welcome() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
const loginMutation = useMutation({
|
||||||
mutationFn: async (token: string) => {
|
mutationFn: async (token: string): Promise<AuthResponse> => {
|
||||||
const res = await fetch('/api/auth', {
|
const res = await fetch('/api/auth', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ action: 'login', token }),
|
body: JSON.stringify({ action: 'login', token }),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json();
|
const data = (await res.json()) as AuthResponse;
|
||||||
throw new Error(data.error || 'Failed to login');
|
throw new Error(data.error ?? 'Failed to login');
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json() as Promise<AuthResponse>;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
@@ -63,6 +70,12 @@ export default function Welcome() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleTokenLogin();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-md border-zinc-800 bg-zinc-950">
|
<Card className="w-full max-w-md border-zinc-800 bg-zinc-950">
|
||||||
<CardHeader className="space-y-2 text-center">
|
<CardHeader className="space-y-2 text-center">
|
||||||
@@ -93,7 +106,7 @@ export default function Welcome() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-emerald-950">
|
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-emerald-950">
|
||||||
<Sparkles className="h-4 w-4 text-emerald-500" />
|
<Activity className="h-4 w-4 text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-zinc-400">Track positive, neutral, or negative habits</p>
|
<p className="text-zinc-400">Track positive, neutral, or negative habits</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +117,9 @@ export default function Welcome() {
|
|||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => createAccountMutation.mutate()}
|
onClick={() => {
|
||||||
|
createAccountMutation.mutate();
|
||||||
|
}}
|
||||||
disabled={createAccountMutation.isPending}
|
disabled={createAccountMutation.isPending}
|
||||||
className="w-full bg-emerald-600 text-white hover:bg-emerald-700"
|
className="w-full bg-emerald-600 text-white hover:bg-emerald-700"
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -113,7 +128,7 @@ export default function Welcome() {
|
|||||||
<>Creating your account...</>
|
<>Creating your account...</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Sparkles className="mr-2 h-4 w-4" />
|
<CalendarCheck className="mr-2 h-4 w-4" />
|
||||||
Start Tracking Now
|
Start Tracking Now
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -130,7 +145,9 @@ export default function Welcome() {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowTokenInput(true)}
|
onClick={() => {
|
||||||
|
setShowTokenInput(true);
|
||||||
|
}}
|
||||||
className="w-full border-zinc-800 hover:bg-zinc-900"
|
className="w-full border-zinc-800 hover:bg-zinc-900"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
@@ -161,8 +178,10 @@ export default function Welcome() {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g., happy-blue-cat-1234"
|
placeholder="e.g., happy-blue-cat-1234"
|
||||||
value={tokenInput}
|
value={tokenInput}
|
||||||
onChange={(e) => setTokenInput(e.target.value)}
|
onChange={(e) => {
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleTokenLogin()}
|
setTokenInput(e.target.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
className="border-zinc-800 bg-zinc-900 placeholder:text-zinc-600"
|
className="border-zinc-800 bg-zinc-900 placeholder:text-zinc-600"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
@@ -1,11 +1,17 @@
|
|||||||
import { defineConfig } from 'drizzle-kit';
|
import { defineConfig } from 'drizzle-kit';
|
||||||
import { env } from 'node:process';
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config({ path: ['.env.local', '.env'] });
|
||||||
|
|
||||||
|
const DATABASE_URL = process.env.POSTGRES_URL;
|
||||||
|
if (!DATABASE_URL) {
|
||||||
|
throw new Error('POSTGRES_URL environment variable is required');
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: './lib/db/schema.ts',
|
schema: './lib/db/schema.ts',
|
||||||
dialect: 'postgresql',
|
dialect: 'postgresql',
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: env.POSTGRES_URL!,
|
url: DATABASE_URL,
|
||||||
},
|
},
|
||||||
out: './drizzle',
|
out: './drizzle',
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,14 @@
|
|||||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
import * as schema from './schema';
|
import * as schema from './schema';
|
||||||
import 'dotenv/config';
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config({ path: ['.env.local', '.env'] });
|
||||||
|
|
||||||
export const db = drizzle(process.env.POSTGRES_URL!, { schema });
|
const DATABASE_URL = process.env.POSTGRES_URL;
|
||||||
|
if (!DATABASE_URL) {
|
||||||
|
throw new Error('POSTGRES_URL environment variable is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const db = drizzle(DATABASE_URL, { schema });
|
||||||
|
|
||||||
// Re-export schema types for convenience
|
// Re-export schema types for convenience
|
||||||
export * from './schema';
|
export * from './schema';
|
||||||
|
@@ -28,9 +28,7 @@ export const habits = pgTable(
|
|||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
archivedAt: timestamp('archived_at'),
|
archivedAt: timestamp('archived_at'),
|
||||||
},
|
},
|
||||||
(table) => ({
|
(table) => [index('habits_user_id_idx').on(table.userId)],
|
||||||
userIdIdx: index('habits_user_id_idx').on(table.userId),
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const habitLogs = pgTable(
|
export const habitLogs = pgTable(
|
||||||
@@ -43,10 +41,10 @@ export const habitLogs = pgTable(
|
|||||||
loggedAt: timestamp('logged_at').defaultNow().notNull(),
|
loggedAt: timestamp('logged_at').defaultNow().notNull(),
|
||||||
note: text('note'),
|
note: text('note'),
|
||||||
},
|
},
|
||||||
(table) => ({
|
(table) => [
|
||||||
habitIdIdx: index('habit_logs_habit_id_idx').on(table.habitId),
|
index('habit_logs_habit_id_idx').on(table.habitId),
|
||||||
loggedAtIdx: index('habit_logs_logged_at_idx').on(table.loggedAt),
|
index('habit_logs_logged_at_idx').on(table.loggedAt),
|
||||||
}),
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
|
Reference in New Issue
Block a user