Fixed a number of issues

This commit is contained in:
2026-01-01 00:51:00 -07:00
parent 4616645939
commit 756ab2a38f
14 changed files with 648 additions and 141 deletions

View File

@@ -0,0 +1,71 @@
import type { APIRoute } from 'astro';
import { db } from '../../../../db';
import { clients, members, timeEntries, timeEntryTags } from '../../../../db/schema';
import { eq, and, inArray } from 'drizzle-orm';
export const POST: APIRoute = async ({ params, locals, redirect }) => {
const user = locals.user;
if (!user) {
return redirect('/login');
}
const { id } = params;
if (!id) {
return new Response('Client ID is required', { status: 400 });
}
try {
// Get the client to check organization ownership
const client = await db.select()
.from(clients)
.where(eq(clients.id, id))
.get();
if (!client) {
return new Response('Client not found', { status: 404 });
}
// Verify user is a member of the organization
const membership = await db.select()
.from(members)
.where(and(
eq(members.userId, user.id),
eq(members.organizationId, client.organizationId)
))
.get();
if (!membership) {
return new Response('Not authorized', { status: 403 });
}
// Find all time entries for this client to clean up tags
const clientEntries = await db.select({ id: timeEntries.id })
.from(timeEntries)
.where(eq(timeEntries.clientId, id))
.all();
const entryIds = clientEntries.map(e => e.id);
if (entryIds.length > 0) {
// Delete tags associated with these entries
await db.delete(timeEntryTags)
.where(inArray(timeEntryTags.timeEntryId, entryIds))
.run();
// Delete the time entries
await db.delete(timeEntries)
.where(eq(timeEntries.clientId, id))
.run();
}
// Delete the client
await db.delete(clients)
.where(eq(clients.id, id))
.run();
return redirect('/dashboard/clients');
} catch (error) {
console.error('Error deleting client:', error);
return new Response('Failed to delete client', { status: 500 });
}
};

View File

@@ -0,0 +1,68 @@
import type { APIRoute } from "astro";
import { db } from "../../../../db";
import { clients, members } from "../../../../db/schema";
import { eq, and } from "drizzle-orm";
export const POST: APIRoute = async ({ request, params, locals, redirect }) => {
const user = locals.user;
if (!user) {
return redirect("/login");
}
const { id } = params;
if (!id) {
return new Response("Client ID is required", { status: 400 });
}
const formData = await request.formData();
const name = formData.get("name") as string;
const email = formData.get("email") as string;
if (!name || name.trim().length === 0) {
return new Response("Client name is required", { status: 400 });
}
try {
// Get the client to check organization ownership
const client = await db
.select()
.from(clients)
.where(eq(clients.id, id))
.get();
if (!client) {
return new Response("Client not found", { status: 404 });
}
// Verify user is a member of the organization
const membership = await db
.select()
.from(members)
.where(
and(
eq(members.userId, user.id),
eq(members.organizationId, client.organizationId),
),
)
.get();
if (!membership) {
return new Response("Not authorized", { status: 403 });
}
// Update client
await db
.update(clients)
.set({
name: name.trim(),
email: email?.trim() || null,
})
.where(eq(clients.id, id))
.run();
return redirect(`/dashboard/clients/${id}`);
} catch (error) {
console.error("Error updating client:", error);
return new Response("Failed to update client", { status: 500 });
}
};

View File

@@ -1,47 +1,59 @@
import type { APIRoute } from 'astro';
import { db } from '../../../db';
import { organizations, members } from '../../../db/schema';
import { eq } from 'drizzle-orm';
import type { APIRoute } from "astro";
import { db } from "../../../db";
import { organizations, members } from "../../../db/schema";
import { eq, and } from "drizzle-orm";
export const POST: APIRoute = async ({ request, locals, redirect }) => {
const user = locals.user;
if (!user) {
return redirect('/login');
return redirect("/login");
}
const formData = await request.formData();
const organizationId = formData.get('organizationId') as string;
const name = formData.get('name') as string;
const organizationId = formData.get("organizationId") as string;
const name = formData.get("name") as string;
if (!organizationId || !name || name.trim().length === 0) {
return new Response('Organization ID and name are required', { status: 400 });
return new Response("Organization ID and name are required", {
status: 400,
});
}
try {
// Verify user is admin/owner of this organization
const membership = await db.select()
const membership = await db
.select()
.from(members)
.where(eq(members.userId, user.id))
.where(
and(
eq(members.userId, user.id),
eq(members.organizationId, organizationId),
),
)
.get();
if (!membership || membership.organizationId !== organizationId) {
return new Response('Not authorized', { status: 403 });
if (!membership) {
return new Response("Not authorized", { status: 403 });
}
const isAdmin = membership.role === 'owner' || membership.role === 'admin';
const isAdmin = membership.role === "owner" || membership.role === "admin";
if (!isAdmin) {
return new Response('Only owners and admins can update organization settings', { status: 403 });
return new Response(
"Only owners and admins can update organization settings",
{ status: 403 },
);
}
// Update organization name
await db.update(organizations)
await db
.update(organizations)
.set({ name: name.trim() })
.where(eq(organizations.id, organizationId))
.run();
return redirect('/dashboard/team/settings?success=org-name');
return redirect("/dashboard/team/settings?success=org-name");
} catch (error) {
console.error('Error updating organization name:', error);
return new Response('Failed to update organization name', { status: 500 });
console.error("Error updating organization name:", error);
return new Response("Failed to update organization name", { status: 500 });
}
};

View File

@@ -0,0 +1,119 @@
---
import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import { db } from '../../../../db';
import { clients, members } from '../../../../db/schema';
import { eq, and } from 'drizzle-orm';
const user = Astro.locals.user;
if (!user) return Astro.redirect('/login');
const { id } = Astro.params;
if (!id) return Astro.redirect('/dashboard/clients');
// Get current team from cookie
const currentTeamId = Astro.cookies.get('currentTeamId')?.value;
const userMemberships = await db.select()
.from(members)
.where(eq(members.userId, user.id))
.all();
if (userMemberships.length === 0) return Astro.redirect('/dashboard');
// Use current team or fallback to first membership
const userMembership = currentTeamId
? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
: userMemberships[0];
const client = await db.select()
.from(clients)
.where(and(
eq(clients.id, id),
eq(clients.organizationId, userMembership.organizationId)
))
.get();
if (!client) return Astro.redirect('/dashboard/clients');
---
<DashboardLayout title={`Edit ${client.name} - Chronus`}>
<div class="max-w-2xl mx-auto">
<div class="flex items-center gap-3 mb-6">
<a href={`/dashboard/clients/${client.id}`} class="btn btn-ghost btn-sm">
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
</a>
<h1 class="text-3xl font-bold">Edit Client</h1>
</div>
<form method="POST" action={`/api/clients/${client.id}/update`} class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body">
<div class="form-control">
<label class="label" for="name">
<span class="label-text">Client Name</span>
</label>
<input
type="text"
id="name"
name="name"
value={client.name}
placeholder="Acme Corp"
class="input input-bordered"
required
/>
</div>
<div class="form-control">
<label class="label" for="email">
<span class="label-text">Email (optional)</span>
</label>
<input
type="email"
id="email"
name="email"
value={client.email || ''}
placeholder="contact@acme.com"
class="input input-bordered"
/>
</div>
<div class="card-actions justify-between mt-6">
<button
type="button"
class="btn btn-error btn-outline"
onclick={`document.getElementById('delete_modal').showModal()`}
>
Delete Client
</button>
<div class="flex gap-2">
<a href={`/dashboard/clients/${client.id}`} class="btn btn-ghost">Cancel</a>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</div>
</div>
</form>
</div>
<!-- Delete Confirmation Modal -->
<dialog id="delete_modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg text-error">Delete Client?</h3>
<p class="py-4">
Are you sure you want to delete <strong>{client.name}</strong>?
This action cannot be undone and will delete all associated time entries.
</p>
<div class="modal-action">
<form method="dialog">
<button class="btn">Cancel</button>
</form>
<form method="POST" action={`/api/clients/${client.id}/delete`}>
<button type="submit" class="btn btn-error">Delete</button>
</form>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</DashboardLayout>

View File

@@ -0,0 +1,203 @@
---
import DashboardLayout from '../../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import { db } from '../../../../db';
import { clients, timeEntries, members, categories, users } from '../../../../db/schema';
import { eq, and, desc, sql } from 'drizzle-orm';
import { formatTimeRange } from '../../../../lib/formatTime';
const user = Astro.locals.user;
if (!user) return Astro.redirect('/login');
const { id } = Astro.params;
if (!id) return Astro.redirect('/dashboard/clients');
// Get current team from cookie
const currentTeamId = Astro.cookies.get('currentTeamId')?.value;
const userMemberships = await db.select()
.from(members)
.where(eq(members.userId, user.id))
.all();
if (userMemberships.length === 0) return Astro.redirect('/dashboard');
// Use current team or fallback to first membership
const userMembership = currentTeamId
? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
: userMemberships[0];
const client = await db.select()
.from(clients)
.where(and(
eq(clients.id, id),
eq(clients.organizationId, userMembership.organizationId)
))
.get();
if (!client) return Astro.redirect('/dashboard/clients');
// Get recent activity
const recentEntries = await db.select({
entry: timeEntries,
category: categories,
user: users,
})
.from(timeEntries)
.leftJoin(categories, eq(timeEntries.categoryId, categories.id))
.leftJoin(users, eq(timeEntries.userId, users.id))
.where(eq(timeEntries.clientId, client.id))
.orderBy(desc(timeEntries.startTime))
.limit(10)
.all();
// Calculate total time tracked
const totalTimeResult = await db.select({
totalDuration: sql<number>`sum(CASE WHEN ${timeEntries.endTime} IS NOT NULL THEN ${timeEntries.endTime} - ${timeEntries.startTime} ELSE 0 END)`
})
.from(timeEntries)
.where(eq(timeEntries.clientId, client.id))
.get();
const totalDurationMs = totalTimeResult?.totalDuration || 0;
const totalHours = Math.floor(totalDurationMs / (1000 * 60 * 60));
const totalMinutes = Math.floor((totalDurationMs % (1000 * 60 * 60)) / (1000 * 60));
// Get total entries count
const totalEntriesResult = await db.select({ count: sql<number>`count(*)` })
.from(timeEntries)
.where(eq(timeEntries.clientId, client.id))
.get();
const totalEntriesCount = totalEntriesResult?.count || 0;
---
<DashboardLayout title={`${client.name} - Clients - Chronus`}>
<div class="flex items-center gap-3 mb-6">
<a href="/dashboard/clients" class="btn btn-ghost btn-sm">
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
</a>
<h1 class="text-3xl font-bold">{client.name}</h1>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- Client Details Card -->
<div class="card bg-base-100 shadow-xl border border-base-200 lg:col-span-2">
<div class="card-body">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-2xl mb-1">{client.name}</h2>
{client.email && (
<div class="flex items-center gap-2 text-base-content/70 mb-4">
<Icon name="heroicons:envelope" class="w-4 h-4" />
<a href={`mailto:${client.email}`} class="link link-hover">{client.email}</a>
</div>
)}
</div>
<div class="flex gap-2">
<a href={`/dashboard/clients/${client.id}/edit`} class="btn btn-primary btn-sm">
<Icon name="heroicons:pencil" class="w-4 h-4" />
Edit
</a>
<form method="POST" action={`/api/clients/${client.id}/delete`} onsubmit="return confirm('Are you sure you want to delete this client? This will also delete all associated time entries.');">
<button type="submit" class="btn btn-error btn-outline btn-sm">
<Icon name="heroicons:trash" class="w-4 h-4" />
Delete
</button>
</form>
</div>
</div>
<div class="divider"></div>
<div class="stats shadow w-full">
<div class="stat">
<div class="stat-figure text-primary">
<Icon name="heroicons:clock" class="w-8 h-8" />
</div>
<div class="stat-title">Total Time Tracked</div>
<div class="stat-value text-primary">{totalHours}h {totalMinutes}m</div>
<div class="stat-desc">Across all projects</div>
</div>
<div class="stat">
<div class="stat-figure text-secondary">
<Icon name="heroicons:list-bullet" class="w-8 h-8" />
</div>
<div class="stat-title">Total Entries</div>
<div class="stat-value text-secondary">{totalEntriesCount}</div>
<div class="stat-desc">Recorded entries</div>
</div>
</div>
</div>
</div>
<!-- Meta Info Card -->
<div class="card bg-base-100 shadow-xl border border-base-200 h-fit">
<div class="card-body">
<h3 class="card-title text-lg mb-4">Information</h3>
<div class="space-y-4">
<div>
<div class="text-sm font-medium text-base-content/60">Created</div>
<div>{client.createdAt?.toLocaleDateString() ?? 'N/A'}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body">
<h2 class="card-title mb-4">Recent Activity</h2>
{recentEntries.length > 0 ? (
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Description</th>
<th>Category</th>
<th>User</th>
<th>Date</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{recentEntries.map(({ entry, category, user: entryUser }) => (
<tr>
<td>{entry.description || '-'}</td>
<td>
{category ? (
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" style={`background-color: ${category.color}`}></span>
<span>{category.name}</span>
</div>
) : '-'}
</td>
<td>{entryUser?.name || 'Unknown'}</td>
<td>{entry.startTime.toLocaleDateString()}</td>
<td class="font-mono">{formatTimeRange(entry.startTime, entry.endTime)}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div class="text-center py-8 text-base-content/60">
No time entries recorded for this client yet.
</div>
)}
{recentEntries.length > 0 && (
<div class="card-actions justify-center mt-4">
<a href={`/dashboard/tracker?client=${client.id}`} class="btn btn-ghost btn-sm">
View All Entries
</a>
</div>
)}
</div>
</div>
</DashboardLayout>

View File

@@ -24,7 +24,7 @@ const userOrgs = await db.select({
.all();
// Use current team or fallback to first
const currentOrg = currentTeamId
const currentOrg = currentTeamId
? userOrgs.find(o => o.organizationId === currentTeamId) || userOrgs[0]
: userOrgs[0];
@@ -78,14 +78,14 @@ if (currentOrg) {
isNull(timeEntries.endTime)
))
.all();
stats.activeTimers = activeCount.length;
const clientCount = await db.select()
.from(clients)
.where(eq(clients.organizationId, currentOrg.organizationId))
.all();
stats.totalClients = clientCount.length;
stats.recentEntries = await db.select({
@@ -109,7 +109,7 @@ const hasMembership = userOrgs.length > 0;
<DashboardLayout title="Dashboard - Chronus">
<div class="flex justify-between items-center mb-8">
<div>
<h1 class="text-4xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent mb-2">
<h1 class="text-4xl font-bold bg-linear-to-r from-primary to-secondary bg-clip-text text-transparent mb-2">
Dashboard
</h1>
<p class="text-base-content/60">Welcome back, {user.name}!</p>
@@ -174,7 +174,7 @@ const hasMembership = userOrgs.length > 0;
<div class="stat-desc">Total active</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Quick Actions -->
<div class="card bg-base-100 shadow-xl">

View File

@@ -19,7 +19,7 @@ const userMemberships = await db.select()
if (userMemberships.length === 0) return Astro.redirect('/dashboard');
// Use current team or fallback to first membership
const userMembership = currentTeamId
const userMembership = currentTeamId
? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
: userMemberships[0];
@@ -101,7 +101,7 @@ const isAdmin = currentUserMember?.member.role === 'owner' || currentUserMember?
<label tabindex="0" class="btn btn-ghost btn-sm">
<Icon name="heroicons:ellipsis-vertical" class="w-5 h-5" />
</label>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 border border-base-200">
<ul tabindex="0" class="dropdown-content z-1 menu p-2 shadow bg-base-100 rounded-box w-52 border border-base-200">
<li>
<form method="POST" action={`/api/team/change-role`}>
<input type="hidden" name="userId" value={teamUser.id} />

View File

@@ -8,12 +8,20 @@ import { eq } from 'drizzle-orm';
const user = Astro.locals.user;
if (!user) return Astro.redirect('/login');
const userMembership = await db.select()
// Get current team from cookie
const currentTeamId = Astro.cookies.get('currentTeamId')?.value;
const userMemberships = await db.select()
.from(members)
.where(eq(members.userId, user.id))
.get();
.all();
if (!userMembership) return Astro.redirect('/dashboard');
if (userMemberships.length === 0) return Astro.redirect('/dashboard');
// Use current team or fallback to first membership
const userMembership = currentTeamId
? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
: userMemberships[0];
const isAdmin = userMembership.role === 'owner' || userMembership.role === 'admin';
if (!isAdmin) return Astro.redirect('/dashboard/team');
@@ -22,7 +30,7 @@ if (!isAdmin) return Astro.redirect('/dashboard/team');
<DashboardLayout title="Invite Team Member - Chronus">
<div class="max-w-2xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Invite Team Member</h1>
<form method="POST" action="/api/team/invite" class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body">
<div class="alert alert-info mb-4">
@@ -34,13 +42,13 @@ if (!isAdmin) return Astro.redirect('/dashboard/team');
<label class="label" for="email">
<span class="label-text">Email Address</span>
</label>
<input
type="email"
<input
type="email"
id="email"
name="email"
placeholder="user@example.com"
class="input input-bordered"
required
name="email"
placeholder="user@example.com"
class="input input-bordered"
required
/>
</div>

View File

@@ -11,18 +11,22 @@ if (!user) return Astro.redirect('/login');
// Get current team from cookie
const currentTeamId = Astro.cookies.get('currentTeamId')?.value;
const userMembership = await db.select()
const userMemberships = await db.select()
.from(members)
.where(eq(members.userId, user.id))
.get();
.all();
if (!userMembership) return Astro.redirect('/dashboard');
if (userMemberships.length === 0) return Astro.redirect('/dashboard');
// Use current team or fallback to first membership
const userMembership = currentTeamId
? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
: userMemberships[0];
const isAdmin = userMembership.role === 'owner' || userMembership.role === 'admin';
if (!isAdmin) return Astro.redirect('/dashboard/team');
// Use current team or fallback to membership org
const orgId = currentTeamId || userMembership.organizationId;
const orgId = userMembership.organizationId;
const organization = await db.select()
.from(organizations)
@@ -55,34 +59,34 @@ const successType = url.searchParams.get('success');
<Icon name="heroicons:building-office-2" class="w-6 h-6" />
Team Settings
</h2>
{successType === 'org-name' && (
<div class="alert alert-success mb-4">
<Icon name="heroicons:check-circle" class="w-6 h-6" />
<span>Team name updated successfully!</span>
</div>
)}
<form action="/api/organizations/update-name" method="POST" class="space-y-4">
<input type="hidden" name="organizationId" value={organization.id} />
<label class="form-control">
<div class="label">
<span class="label-text font-medium">Team Name</span>
</div>
<input
type="text"
name="name"
<input
type="text"
name="name"
value={organization.name}
placeholder="Organization name"
class="input input-bordered w-full"
required
placeholder="Organization name"
class="input input-bordered w-full"
required
/>
<div class="label">
<span class="label-text-alt text-base-content/60">This name is visible to all team members</span>
</div>
</label>
<div class="flex justify-end">
<button type="submit" class="btn btn-primary">
<Icon name="heroicons:check" class="w-5 h-5" />
@@ -106,7 +110,7 @@ const successType = url.searchParams.get('success');
Add Category
</a>
</div>
<p class="text-base-content/70 mb-4">
Categories help organize time tracking by type of work. All team members use the same categories.
</p>
@@ -126,16 +130,16 @@ const successType = url.searchParams.get('success');
<div class="card-body p-4">
<div class="flex items-center gap-3">
{category.color && (
<span class="w-4 h-4 rounded-full flex-shrink-0" style={`background-color: ${category.color}`}></span>
<span class="w-4 h-4 rounded-full shrink-0" style={`background-color: ${category.color}`}></span>
)}
<div class="flex-grow min-w-0">
<div class="grow min-w-0">
<h3 class="font-semibold truncate">{category.name}</h3>
<p class="text-xs text-base-content/60">
Created {category.createdAt?.toLocaleDateString() ?? 'N/A'}
</p>
</div>
<a
href={`/dashboard/team/settings/categories/${category.id}/edit`}
<a
href={`/dashboard/team/settings/categories/${category.id}/edit`}
class="btn btn-ghost btn-xs"
>
<Icon name="heroicons:pencil" class="w-4 h-4" />

View File

@@ -10,12 +10,20 @@ if (!user) return Astro.redirect('/login');
const { id } = Astro.params;
const userMembership = await db.select()
// Get current team from cookie
const currentTeamId = Astro.cookies.get('currentTeamId')?.value;
const userMemberships = await db.select()
.from(members)
.where(eq(members.userId, user.id))
.get();
.all();
if (!userMembership) return Astro.redirect('/dashboard');
if (userMemberships.length === 0) return Astro.redirect('/dashboard');
// Use current team or fallback to first membership
const userMembership = currentTeamId
? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0]
: userMemberships[0];
const isAdmin = userMembership.role === 'owner' || userMembership.role === 'admin';
if (!isAdmin) return Astro.redirect('/dashboard/team/settings');
@@ -39,21 +47,21 @@ if (!category) return Astro.redirect('/dashboard/team/settings');
</a>
<h1 class="text-3xl font-bold">Edit Category</h1>
</div>
<form method="POST" action={`/api/categories/${id}/update`} class="card bg-base-200 shadow-xl border border-base-300">
<div class="card-body">
<div class="form-control">
<label class="label pb-2" for="name">
<span class="label-text font-medium">Category Name</span>
</label>
<input
type="text"
<input
type="text"
id="name"
name="name"
name="name"
value={category.name}
placeholder="Development"
class="input input-bordered w-full"
required
placeholder="Development"
class="input input-bordered w-full"
required
/>
</div>
@@ -61,12 +69,12 @@ if (!category) return Astro.redirect('/dashboard/team/settings');
<label class="label pb-2" for="color">
<span class="label-text font-medium">Color (optional)</span>
</label>
<input
type="color"
<input
type="color"
id="color"
name="color"
value={category.color || '#3b82f6'}
class="input input-bordered w-full h-12"
class="input input-bordered w-full h-12"
/>
</div>

View File

@@ -7,11 +7,11 @@ if (Astro.locals.user) {
---
<Layout title="Chronus - Time Tracking">
<div class="hero min-h-screen bg-gradient-to-br from-base-100 via-base-200 to-base-300">
<div class="hero min-h-screen bg-linear-to-br from-base-100 via-base-200 to-base-300">
<div class="hero-content text-center">
<div class="max-w-4xl">
<img src="/src/assets/logo.webp" alt="Chronus Logo" class="h-24 w-24 mx-auto mb-6" />
<h1 class="text-6xl md:text-7xl font-bold mb-6 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
<h1 class="text-6xl md:text-7xl font-bold mb-6 bg-linear-to-r from-primary to-secondary bg-clip-text text-transparent">
Chronus
</h1>
<p class="text-xl md:text-2xl py-6 text-base-content/80 font-light max-w-2xl mx-auto">
@@ -26,28 +26,28 @@ if (Astro.locals.user) {
</a>
<a href="/login" class="btn btn-outline btn-lg">Login</a>
</div>
<!-- Feature highlights -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16">
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body items-start">
<div class="text-4xl mb-3">⚡</div>
<h3 class="card-title text-lg">Lightning Fast</h3>
<p class="text-sm text-base-content/70">Track time with a single click. No complexity, just efficiency.</p>
<p class="text-sm text-base-content/70">Track tasks with a single click.</p>
</div>
</div>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body items-start">
<div class="text-4xl mb-3">📊</div>
<h3 class="card-title text-lg">Insightful Reports</h3>
<p class="text-sm text-base-content/70">Understand where time goes with beautiful, actionable insights.</p>
<h3 class="card-title text-lg">Detailed Reports</h3>
<p class="text-sm text-base-content/70">Get actionable insights into your team's tasks.</p>
</div>
</div>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
<div class="card-body items-start">
<div class="text-4xl mb-3">👥</div>
<h3 class="card-title text-lg">Team Collaboration</h3>
<p class="text-sm text-base-content/70">Built for teams that need to track, manage, and analyze together.</p>
<p class="text-sm text-base-content/70">Built for multiple team members.</p>
</div>
</div>
</div>