Refactor monorepo structure and replace hardcoded colors
Add TypeScript project references, update ESLint configs to support ESM modules, and introduce brand semantic color tokens across the web app. Add comprehensive documentation for architecture, API reference, development setup, and user guide.
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
@@ -14,6 +18,11 @@ const eslintConfig = defineConfig([
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: path.resolve(__dirname),
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Allow _-prefixed parameters and variables to signal intentionally unused.
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
|
||||
@@ -15,9 +15,9 @@ export default function CalendarPage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl p-4 sm:p-6 lg:p-8">
|
||||
<div className="mb-6 rounded-2xl border border-white/10 bg-[#17112A]/55 p-5 shadow-[0_22px_70px_rgba(0,0,0,0.24)] backdrop-blur-xl">
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.28em] text-[#D946EF]">Calendar sync</p>
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.28em] text-brand-fuchsia">Calendar sync</p>
|
||||
<h1 className="text-3xl font-extrabold text-white sm:text-4xl">Import, inspect, and time your day.</h1>
|
||||
<p className="mt-2 text-[#F4F1EA]/66">View and manage every appointment from a single departure-focused calendar.</p>
|
||||
<p className="mt-2 text-brand-light/66">View and manage every appointment from a single departure-focused calendar.</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
|
||||
@@ -32,7 +32,7 @@ const BikeSection: React.FC<BikeSectionProps> = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Bicycle Route</h3>
|
||||
<p className="text-sm text-[#F4F1EA]/60">Door-to-door route to the event</p>
|
||||
<p className="text-sm text-brand-light/60">Door-to-door route to the event</p>
|
||||
</div>
|
||||
{onRefresh && (
|
||||
<Button variant="secondary" size="sm" onClick={onRefresh}>
|
||||
@@ -47,29 +47,29 @@ const BikeSection: React.FC<BikeSectionProps> = ({
|
||||
<LoadingSpinner size="md" />
|
||||
</div>
|
||||
) : bikeError ? (
|
||||
<div className="text-center py-4 text-[#FF2D8D]">Error: {bikeError}</div>
|
||||
<div className="text-center py-4 text-brand-pink">Error: {bikeError}</div>
|
||||
) : bikeRoute ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[#F4F1EA]/66">Distance</span>
|
||||
<span className="text-brand-light/66">Distance</span>
|
||||
<span className="font-medium text-white">
|
||||
{Math.round(bikeRoute.distance / 1000)} km ({Math.round(bikeRoute.distance)} m)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[#F4F1EA]/66">Duration</span>
|
||||
<span className="text-brand-light/66">Duration</span>
|
||||
<span className="font-medium text-white">
|
||||
{Math.floor(bikeRoute.duration / 60)} min {bikeRoute.duration % 60} s
|
||||
</span>
|
||||
</div>
|
||||
{bikeRoute.steps && bikeRoute.steps.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="mb-2 font-medium text-[#F4F1EA]">Steps</h4>
|
||||
<h4 className="mb-2 font-medium text-brand-light">Steps</h4>
|
||||
<ul className="space-y-2">
|
||||
{bikeRoute.steps.map((step, index) => (
|
||||
<li key={index} className="text-sm">
|
||||
<span className="font-medium text-[#D946EF]">{step.name}</span>
|
||||
<span className="ml-2 text-[#F4F1EA]/70">{step.instruction}</span>
|
||||
<span className="font-medium text-brand-fuchsia">{step.name}</span>
|
||||
<span className="ml-2 text-brand-light/70">{step.instruction}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -77,7 +77,7 @@ const BikeSection: React.FC<BikeSectionProps> = ({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-4 text-center text-sm text-[#F4F1EA]/58">
|
||||
<div className="py-4 text-center text-sm text-brand-light/58">
|
||||
Add origin and destination coordinates to calculate a bike route.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function EventCard({ event, originStation }: EventCardProps) {
|
||||
<div className="brand-panel overflow-hidden rounded-2xl p-4 sm:p-5">
|
||||
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<p className="mb-1 text-xs font-semibold uppercase tracking-[0.24em] text-[#D946EF]">Next stop</p>
|
||||
<p className="mb-1 text-xs font-semibold uppercase tracking-[0.24em] text-brand-fuchsia">Next stop</p>
|
||||
<h3 className="text-2xl font-bold text-white">{event.title}</h3>
|
||||
</div>
|
||||
<CountdownBadge countdown={countdown} status={status} />
|
||||
@@ -94,12 +94,12 @@ export default function EventCard({ event, originStation }: EventCardProps) {
|
||||
|
||||
<div className="mb-5 grid gap-3 sm:grid-cols-2">
|
||||
<div className="brand-panel-soft rounded-xl p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-[#F4F1EA]/42">Destination</p>
|
||||
<p className="mt-1 text-sm font-medium text-[#F4F1EA]">{event.destination}</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-brand-light/42">Destination</p>
|
||||
<p className="mt-1 text-sm font-medium text-brand-light">{event.destination}</p>
|
||||
</div>
|
||||
<div className="brand-panel-soft rounded-xl p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-[#F4F1EA]/42">Appointment</p>
|
||||
<p className="mt-1 text-sm font-medium text-[#F4F1EA]">{format(event.eventTime, "EEE dd MMM yyyy HH:mm")}</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-brand-light/42">Appointment</p>
|
||||
<p className="mt-1 text-sm font-medium text-brand-light">{format(event.eventTime, "EEE dd MMM yyyy HH:mm")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,8 +109,8 @@ export default function EventCard({ event, originStation }: EventCardProps) {
|
||||
key={option.id}
|
||||
className={`rounded-xl border p-3 text-left transition-all ${
|
||||
activeMode === option.id
|
||||
? "border-[#D946EF]/70 bg-[#B23CFF]/18 shadow-[0_14px_34px_rgba(178,60,255,0.2)]"
|
||||
: "border-white/10 bg-white/[0.045] hover:border-[#D946EF]/40 hover:bg-white/[0.07]"
|
||||
? "border-brand-fuchsia/70 bg-brand-purple/18 shadow-[0_14px_34px_rgba(178,60,255,0.2)]"
|
||||
: "border-white/10 bg-white/[0.045] hover:border-brand-fuchsia/40 hover:bg-white/[0.07]"
|
||||
} ${option.disabled ? "cursor-not-allowed opacity-45 hover:border-white/10 hover:bg-white/[0.045]" : ""}`}
|
||||
onClick={() => {
|
||||
if (!option.disabled) {
|
||||
@@ -122,31 +122,31 @@ export default function EventCard({ event, originStation }: EventCardProps) {
|
||||
<span className="flex items-center justify-between gap-3">
|
||||
<span className="font-semibold text-white">{option.label}</span>
|
||||
{activeMode === option.id && (
|
||||
<span className="rounded-full bg-[#FF2D8D]/20 px-2 py-0.5 text-xs font-semibold text-pink-100">
|
||||
<span className="rounded-full bg-brand-pink/20 px-2 py-0.5 text-xs font-semibold text-pink-100">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs text-[#F4F1EA]/58">{option.meta}</span>
|
||||
<span className="mt-1 block text-xs text-brand-light/58">{option.meta}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mb-4 grid gap-3 rounded-xl border border-white/10 bg-black/16 p-3 text-sm sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-[#F4F1EA]/42">Leave by</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-brand-light/42">Leave by</p>
|
||||
<p className="mt-1 font-semibold text-white">
|
||||
{departureTime ? format(departureTime, "HH:mm") : "Pending"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-[#F4F1EA]/42">Arrive by</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-brand-light/42">Arrive by</p>
|
||||
<p className="mt-1 font-semibold text-white">
|
||||
{arrivalTime ? format(arrivalTime, "HH:mm") : format(new Date(event.eventTime.getTime() - arrivalBufferMinutes * 60_000), "HH:mm")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-[#F4F1EA]/42">Buffer</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-brand-light/42">Buffer</p>
|
||||
<p className="mt-1 font-semibold text-white">
|
||||
{arrivalBufferMinutes} min {calculatedMode ? `via ${calculatedMode}` : ""}
|
||||
</p>
|
||||
|
||||
@@ -22,7 +22,7 @@ const JourneyList: React.FC<JourneyListProps> = ({
|
||||
const targetArrivalTime = new Date(eventTime.getTime() - arrivalBufferMinutes * 60000);
|
||||
|
||||
if (journeys.length === 0) {
|
||||
return <div className={`py-8 text-center text-[#F4F1EA]/60 ${className}`}>No journeys found</div>;
|
||||
return <div className={`py-8 text-center text-brand-light/60 ${className}`}>No journeys found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -37,20 +37,20 @@ const JourneyList: React.FC<JourneyListProps> = ({
|
||||
key={journey.id}
|
||||
className={`rounded-xl border p-3 ${
|
||||
arrivesTooLate || journey.cancelled
|
||||
? "border-[#FF2D8D]/18 bg-[#FF2D8D]/7 opacity-40 line-through"
|
||||
? "border-brand-pink/18 bg-brand-pink/7 opacity-40 line-through"
|
||||
: "border-white/10 bg-white/[0.04]"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="flex-1">
|
||||
<span className="font-semibold text-white">{formatTime(departure)}</span>
|
||||
<span className="ml-2 text-sm text-[#F4F1EA]/48">{journey.platform}</span>
|
||||
<span className="ml-2 text-sm text-brand-light/48">{journey.platform}</span>
|
||||
{journey.delay > 0 && (
|
||||
<span className="ml-2 text-sm font-medium text-orange-200">{`+${journey.delay}'`}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<span className={`font-medium ${journey.cancelled ? "text-[#FF2D8D]" : "text-[#F4F1EA]"}`}>
|
||||
<span className={`font-medium ${journey.cancelled ? "text-brand-pink" : "text-brand-light"}`}>
|
||||
{journey.cancelled ? "Cancelled" : journey.trains.join(" -> ")}
|
||||
</span>
|
||||
</div>
|
||||
@@ -58,11 +58,11 @@ const JourneyList: React.FC<JourneyListProps> = ({
|
||||
<LeaveByBadge countdown={calculateCountdown(departure)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[#F4F1EA]/64">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-brand-light/64">
|
||||
<span>{journey.changes > 0 ? `Change(s): ${journey.changes}` : "Direct"}</span>
|
||||
<span>Arrives {formatTime(arrival)}</span>
|
||||
{arrivesTooLate && (
|
||||
<span className="font-medium text-[#FF2D8D]">
|
||||
<span className="font-medium text-brand-pink">
|
||||
misses {arrivalBufferMinutes} min buffer
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -43,11 +43,11 @@ const TrainSection: React.FC<TrainSectionProps> = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Trains</h3>
|
||||
<p className="text-sm text-[#F4F1EA]/66">
|
||||
<p className="text-sm text-brand-light/66">
|
||||
To {destName} <span className="font-mono">{formatDateTime(eventTime)}</span>
|
||||
</p>
|
||||
{(arrivalBufferMinutes ?? 0) > 0 && (
|
||||
<p className="mt-1 text-xs text-[#D946EF]">
|
||||
<p className="mt-1 text-xs text-brand-fuchsia">
|
||||
Target arrival: {arrivalBufferMinutes} min early
|
||||
</p>
|
||||
)}
|
||||
@@ -65,7 +65,7 @@ const TrainSection: React.FC<TrainSectionProps> = ({
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-8 text-center text-sm text-[#FF2D8D]">{error}</div>
|
||||
<div className="p-8 text-center text-sm text-brand-pink">{error}</div>
|
||||
) : (
|
||||
<>
|
||||
<JourneyList journeys={journeys} eventTime={eventTime} arrivalBufferMinutes={arrivalBufferMinutes ?? 0} />
|
||||
|
||||
@@ -24,7 +24,7 @@ const WalkingOption: React.FC<WalkingOptionProps> = ({ walkRoute, walkLoading, w
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Final Walk</h3>
|
||||
<p className="text-sm text-[#F4F1EA]/60">From arrival station to destination</p>
|
||||
<p className="text-sm text-brand-light/60">From arrival station to destination</p>
|
||||
</div>
|
||||
{onRefresh && (
|
||||
<Button variant="secondary" size="sm" onClick={onRefresh}>
|
||||
@@ -39,29 +39,29 @@ const WalkingOption: React.FC<WalkingOptionProps> = ({ walkRoute, walkLoading, w
|
||||
<LoadingSpinner size="md" />
|
||||
</div>
|
||||
) : walkError ? (
|
||||
<div className="py-4 text-center text-[#FF2D8D]">Error: {walkError}</div>
|
||||
<div className="py-4 text-center text-brand-pink">Error: {walkError}</div>
|
||||
) : walkRoute ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[#F4F1EA]/66">Distance</span>
|
||||
<span className="text-brand-light/66">Distance</span>
|
||||
<span className="font-medium text-white">
|
||||
{Math.round(walkRoute.distance / 1000)} km ({Math.round(walkRoute.distance)} m)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[#F4F1EA]/66">Duration</span>
|
||||
<span className="text-brand-light/66">Duration</span>
|
||||
<span className="font-medium text-white">
|
||||
{Math.floor(walkRoute.duration / 60)} min {walkRoute.duration % 60} s
|
||||
</span>
|
||||
</div>
|
||||
{walkRoute.steps && walkRoute.steps.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="mb-2 font-medium text-[#F4F1EA]">Steps</h4>
|
||||
<h4 className="mb-2 font-medium text-brand-light">Steps</h4>
|
||||
<ul className="space-y-2">
|
||||
{walkRoute.steps.map((step: { name: string; instruction: string }, index: number) => (
|
||||
<li key={index} className="text-sm">
|
||||
<span className="font-medium text-[#D946EF]">{step.name}</span>
|
||||
<span className="ml-2 text-[#F4F1EA]/70">{step.instruction}</span>
|
||||
<span className="font-medium text-brand-fuchsia">{step.name}</span>
|
||||
<span className="ml-2 text-brand-light/70">{step.instruction}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} h-full antialiased`}>
|
||||
<body className="brand-shell min-h-full flex flex-col bg-[#090816] text-[#F4F1EA]">
|
||||
<body className="brand-shell min-h-full flex flex-col bg-[#090816] text-brand-light">
|
||||
<ReminderSettingsProvider>
|
||||
<EventsProvider>
|
||||
<ReminderEngine />
|
||||
|
||||
@@ -28,14 +28,14 @@ const Header: React.FC<HeaderProps> = ({ className = "" }) => {
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex h-12 items-center rounded-lg text-[#F4F1EA] drop-shadow-[0_0_18px_rgba(178,60,255,0.28)] focus:outline-none focus:ring-2 focus:ring-[#D946EF]/70"
|
||||
className="flex h-12 items-center rounded-lg text-brand-light drop-shadow-[0_0_18px_rgba(178,60,255,0.28)] focus:outline-none focus:ring-2 focus:ring-brand-fuchsia/70"
|
||||
aria-label="TimeToLeave home"
|
||||
>
|
||||
<LogoHorizontal height={42} className="block h-[42px] w-auto" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="hidden md:flex items-center rounded-full border border-white/10 bg-white/[0.06] px-3 py-1.5 text-sm text-[#F4F1EA]/76">
|
||||
<div className="hidden md:flex items-center rounded-full border border-white/10 bg-white/[0.06] px-3 py-1.5 text-sm text-brand-light/76">
|
||||
<span className="font-semibold text-white">
|
||||
{events.length}
|
||||
</span>
|
||||
@@ -46,8 +46,8 @@ const Header: React.FC<HeaderProps> = ({ className = "" }) => {
|
||||
Online
|
||||
</span>
|
||||
) : status === false ? (
|
||||
<span className="ml-3 inline-flex items-center gap-1 text-[#FF2D8D]">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#FF2D8D]" />
|
||||
<span className="ml-3 inline-flex items-center gap-1 text-brand-pink">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-brand-pink" />
|
||||
Offline
|
||||
</span>
|
||||
) : null}
|
||||
@@ -58,7 +58,7 @@ const Header: React.FC<HeaderProps> = ({ className = "" }) => {
|
||||
<button
|
||||
onClick={toggle}
|
||||
aria-label="Toggle dark mode"
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/[0.06] text-sm text-[#F4F1EA]/80 transition-colors hover:border-[#D946EF]/45 hover:bg-[#D946EF]/12 hover:text-white focus:outline-none focus:ring-2 focus:ring-[#D946EF]/60"
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/[0.06] text-sm text-brand-light/80 transition-colors hover:border-brand-fuchsia/45 hover:bg-brand-fuchsia/12 hover:text-white focus:outline-none focus:ring-2 focus:ring-brand-fuchsia/60"
|
||||
>
|
||||
{mounted && dark ? (
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
@@ -74,7 +74,7 @@ const Header: React.FC<HeaderProps> = ({ className = "" }) => {
|
||||
<button
|
||||
onClick={() => setShowSettingsModal(true)}
|
||||
aria-label="Settings"
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/[0.06] text-sm text-[#F4F1EA]/80 transition-colors hover:border-[#D946EF]/45 hover:bg-[#D946EF]/12 hover:text-white focus:outline-none focus:ring-2 focus:ring-[#D946EF]/60"
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/[0.06] text-sm text-brand-light/80 transition-colors hover:border-brand-fuchsia/45 hover:bg-brand-fuchsia/12 hover:text-white focus:outline-none focus:ring-2 focus:ring-brand-fuchsia/60"
|
||||
>
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 15.5A3.5 3.5 0 1 0 12 8a3.5 3.5 0 0 0 0 7.5z" />
|
||||
@@ -90,12 +90,12 @@ const Header: React.FC<HeaderProps> = ({ className = "" }) => {
|
||||
<div className="brand-panel relative w-full max-w-sm rounded-2xl p-6">
|
||||
<button
|
||||
onClick={() => setShowSettingsModal(false)}
|
||||
className="absolute right-3 top-3 inline-flex h-8 w-8 items-center justify-center rounded-lg text-[#F4F1EA]/50 transition-colors hover:bg-white/10 hover:text-white"
|
||||
className="absolute right-3 top-3 inline-flex h-8 w-8 items-center justify-center rounded-lg text-brand-light/50 transition-colors hover:bg-white/10 hover:text-white"
|
||||
aria-label="Close settings"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
<h2 className="mb-4 text-lg font-semibold text-[#F4F1EA]">Settings</h2>
|
||||
<h2 className="mb-4 text-lg font-semibold text-brand-light">Settings</h2>
|
||||
<ReminderSettingsPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function Home() {
|
||||
<section className="mb-7 overflow-hidden rounded-2xl border border-white/10 bg-[#17112A]/55 p-5 shadow-[0_22px_70px_rgba(0,0,0,0.28)] backdrop-blur-xl sm:p-7">
|
||||
<div className="flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.28em] text-[#D946EF]">Departure desk</p>
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.28em] text-brand-fuchsia">Departure desk</p>
|
||||
<h1 className="max-w-2xl text-3xl font-extrabold leading-tight text-white sm:text-5xl">
|
||||
Know when to leave before the clock turns hostile.
|
||||
</h1>
|
||||
@@ -25,22 +25,22 @@ export default function Home() {
|
||||
<div className="grid grid-cols-2 gap-3 sm:min-w-60">
|
||||
<div className="brand-panel-soft rounded-xl p-3">
|
||||
<p className="text-2xl font-bold text-white">{upcoming.length}</p>
|
||||
<p className="text-xs text-[#F4F1EA]/60">upcoming</p>
|
||||
<p className="text-xs text-brand-light/60">upcoming</p>
|
||||
</div>
|
||||
<div className="brand-panel-soft rounded-xl p-3">
|
||||
<p className="text-2xl font-bold text-white">{events.length}</p>
|
||||
<p className="text-xs text-[#F4F1EA]/60">total events</p>
|
||||
<p className="text-xs text-brand-light/60">total events</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{upcoming.length === 0 ? (
|
||||
<div className="brand-panel mx-auto max-w-2xl rounded-2xl px-6 py-14 text-center">
|
||||
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[#8B5CF6] to-[#FF2D8D] text-2xl font-black text-white shadow-[0_16px_44px_rgba(178,60,255,0.4)]">
|
||||
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[#8B5CF6] to-brand-pink text-2xl font-black text-white shadow-[0_16px_44px_rgba(178,60,255,0.4)]">
|
||||
T
|
||||
</div>
|
||||
<p className="mb-2 text-2xl font-bold text-white">No upcoming events</p>
|
||||
<p className="mx-auto max-w-sm text-sm leading-6 text-[#F4F1EA]/66">
|
||||
<p className="mx-auto max-w-sm text-sm leading-6 text-brand-light/66">
|
||||
Add an event or import your calendar to turn this into a live departure board.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`inline-block animate-spin rounded-full border-4 border-solid border-[#B23CFF] border-t-transparent ${sizeClasses[size]} ${className}`
|
||||
`inline-block animate-spin rounded-full border-4 border-solid border-brand-purple border-t-transparent ${sizeClasses[size]} ${className}`
|
||||
}
|
||||
data-testid="loading-spinner"
|
||||
>
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{/* Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-[#F4F1EA]/80">
|
||||
<span className="text-sm font-medium text-brand-light/80">
|
||||
Leave reminders
|
||||
</span>
|
||||
<button
|
||||
@@ -38,7 +38,7 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
aria-checked={enabled}
|
||||
onClick={() => setEnabled(!enabled)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
enabled ? "bg-gradient-to-r from-[#8B5CF6] to-[#FF2D8D]" : "bg-white/16"
|
||||
enabled ? "bg-gradient-to-r from-[#8B5CF6] to-brand-pink" : "bg-white/16"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
@@ -54,10 +54,10 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="buffer-minutes"
|
||||
className="text-sm font-medium text-[#F4F1EA]/80"
|
||||
className="text-sm font-medium text-brand-light/80"
|
||||
>
|
||||
Remind me{" "}
|
||||
<span className="text-[#F4F1EA]/48">
|
||||
<span className="text-brand-light/48">
|
||||
(minutes before event)
|
||||
</span>
|
||||
</label>
|
||||
@@ -70,11 +70,11 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
step={1}
|
||||
value={bufferMinutes}
|
||||
onChange={(e) => setBufferMinutes(Number(e.target.value))}
|
||||
className="flex-1 accent-[#B23CFF]"
|
||||
className="flex-1 accent-brand-purple"
|
||||
/>
|
||||
<output
|
||||
htmlFor="buffer-minutes"
|
||||
className="min-w-[3ch] text-center text-sm font-semibold tabular-nums text-[#F4F1EA]/80"
|
||||
className="min-w-[3ch] text-center text-sm font-semibold tabular-nums text-brand-light/80"
|
||||
>
|
||||
{bufferMinutes}
|
||||
</output>
|
||||
@@ -86,10 +86,10 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="arrival-buffer"
|
||||
className="text-sm font-medium text-[#F4F1EA]/80"
|
||||
className="text-sm font-medium text-brand-light/80"
|
||||
>
|
||||
Arrive early
|
||||
<span className="text-[#F4F1EA]/48">
|
||||
<span className="text-brand-light/48">
|
||||
(minutes before event)
|
||||
</span>
|
||||
</label>
|
||||
@@ -102,11 +102,11 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
step={1}
|
||||
value={arrivalBufferMinutes}
|
||||
onChange={(e) => setArrivalBufferMinutes(Number(e.target.value))}
|
||||
className="flex-1 accent-[#B23CFF]"
|
||||
className="flex-1 accent-brand-purple"
|
||||
/>
|
||||
<output
|
||||
htmlFor="arrival-buffer"
|
||||
className="min-w-[3ch] text-center text-sm font-semibold tabular-nums text-[#F4F1EA]/80"
|
||||
className="min-w-[3ch] text-center text-sm font-semibold tabular-nums text-brand-light/80"
|
||||
>
|
||||
{arrivalBufferMinutes}
|
||||
</output>
|
||||
@@ -115,7 +115,7 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
|
||||
{/* Show walking option toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-[#F4F1EA]/80">
|
||||
<span className="text-sm font-medium text-brand-light/80">
|
||||
Show walking option
|
||||
</span>
|
||||
<button
|
||||
@@ -123,7 +123,7 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
aria-checked={showWalkingOption}
|
||||
onClick={() => setShowWalkingOption(!showWalkingOption)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
showWalkingOption ? "bg-gradient-to-r from-[#8B5CF6] to-[#FF2D8D]" : "bg-white/16"
|
||||
showWalkingOption ? "bg-gradient-to-r from-[#8B5CF6] to-brand-pink" : "bg-white/16"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
@@ -136,7 +136,7 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
|
||||
{/* Show bike option toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-[#F4F1EA]/80">
|
||||
<span className="text-sm font-medium text-brand-light/80">
|
||||
Show bike route
|
||||
</span>
|
||||
<button
|
||||
@@ -144,7 +144,7 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
aria-checked={showBikeOption}
|
||||
onClick={() => setShowBikeOption(!showBikeOption)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
showBikeOption ? "bg-gradient-to-r from-[#8B5CF6] to-[#FF2D8D]" : "bg-white/16"
|
||||
showBikeOption ? "bg-gradient-to-r from-[#8B5CF6] to-brand-pink" : "bg-white/16"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
@@ -157,11 +157,11 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
|
||||
|
||||
{/* Permission status */}
|
||||
<div className="border-t border-white/10 pt-2">
|
||||
<p className="text-xs text-[#F4F1EA]/50">{statusLabel}</p>
|
||||
<p className="text-xs text-brand-light/50">{statusLabel}</p>
|
||||
{permission !== "granted" && permission !== "denied" && (
|
||||
<button
|
||||
onClick={() => Notification.requestPermission()}
|
||||
className="mt-2 text-xs text-[#B23CFF] hover:underline"
|
||||
className="mt-2 text-xs text-brand-purple hover:underline"
|
||||
>
|
||||
Request permission now
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user