Refactor component styles to use CSS utility classes

Convert inline styles to reusable CSS classes for better maintainability
This commit is contained in:
2026-05-18 22:32:09 +02:00
parent 25676bf1e7
commit bb286e1667
16 changed files with 92 additions and 50 deletions
+6 -6
View File
@@ -104,9 +104,9 @@ const AddEventModal: React.FC<AddEventModalProps> = ({ isOpen, onClose, editEven
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-sm"
onClick={handleBackdropClick}
>
<div className={`brand-panel w-full max-w-md rounded-2xl relative ${className}`}>
<div className={`brand-panel relative w-full max-w-md ${className}`}>
<div className="p-6">
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.24em] text-[#D946EF]">
<p className="mb-2 eyebrow">
{isEditing ? "Edit event" : "Manual event"}
</p>
<h3 className="mb-6 text-2xl font-bold text-white">
@@ -123,7 +123,7 @@ const AddEventModal: React.FC<AddEventModalProps> = ({ isOpen, onClose, editEven
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Team Meeting"
className="brand-input px-3 py-2"
className="brand-input"
required
/>
</div>
@@ -137,7 +137,7 @@ const AddEventModal: React.FC<AddEventModalProps> = ({ isOpen, onClose, editEven
value={destination}
onChange={(e) => setDestination(e.target.value)}
placeholder="Technikum Wien or Hoechstaedtplatz 6, 1200 Wien"
className="brand-input px-3 py-2"
className="brand-input"
required
/>
</div>
@@ -151,7 +151,7 @@ const AddEventModal: React.FC<AddEventModalProps> = ({ isOpen, onClose, editEven
type="date"
value={eventDate}
onChange={(e) => setEventDate(e.target.value)}
className="brand-input px-3 py-2"
className="brand-input"
required
/>
</div>
@@ -164,7 +164,7 @@ const AddEventModal: React.FC<AddEventModalProps> = ({ isOpen, onClose, editEven
type="time"
value={eventTime}
onChange={(e) => setEventTime(e.target.value)}
className="brand-input px-3 py-2"
className="brand-input"
required
/>
</div>
+10 -10
View File
@@ -67,9 +67,9 @@ export default function BatchEditPanel() {
if (events.length === 0) return null;
return (
<div className="brand-panel overflow-hidden rounded-2xl">
<div className="border-b border-white/10 p-4">
<p className="mb-1 text-xs font-semibold uppercase tracking-[0.24em] text-brand-fuchsia">Bulk action</p>
<div className="brand-panel overflow-hidden">
<div className="section-header">
<p className="mb-1 eyebrow">Bulk action</p>
<h3 className="text-lg font-semibold text-white">Batch-edit destinations</h3>
<p className="mt-1 text-sm text-[#F4F1EA]/55">
Replace the destination on multiple events at once useful when a calendar stores a room number but you need a full address.
@@ -80,14 +80,14 @@ export default function BatchEditPanel() {
{/* Filters */}
<div className="grid gap-3 sm:grid-cols-2">
<div>
<label htmlFor="batch-source" className="mb-1 block text-xs font-semibold uppercase tracking-[0.18em] text-[#F4F1EA]/55">
<label htmlFor="batch-source" className="mb-1 block label-muted">
Calendar source
</label>
<select
id="batch-source"
value={sourceFilter}
onChange={(e) => { setSourceFilter(e.target.value); setApplied(null); }}
className="brand-input px-3 py-2 text-sm"
className="brand-input text-sm"
>
<option value="__all__">All sources ({events.length} events)</option>
{sources.map((s) => (
@@ -99,7 +99,7 @@ export default function BatchEditPanel() {
</div>
<div>
<label htmlFor="batch-dest-filter" className="mb-1 block text-xs font-semibold uppercase tracking-[0.18em] text-[#F4F1EA]/55">
<label htmlFor="batch-dest-filter" className="mb-1 block label-muted">
Current destination contains
</label>
<input
@@ -108,14 +108,14 @@ export default function BatchEditPanel() {
value={destContains}
onChange={(e) => { setDestContains(e.target.value); setApplied(null); }}
placeholder="e.g. HS, Seminarraum, Room …"
className="brand-input px-3 py-2 text-sm"
className="brand-input text-sm"
/>
</div>
</div>
{/* Preview */}
<div>
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-[#F4F1EA]/55">
<p className="mb-2 label-muted">
Matched events ({matched.length})
</p>
{matched.length === 0 ? (
@@ -137,7 +137,7 @@ export default function BatchEditPanel() {
{/* Replace with */}
<div className="flex gap-3 items-end">
<div className="flex-1">
<label htmlFor="batch-new-dest" className="mb-1 block text-xs font-semibold uppercase tracking-[0.18em] text-[#F4F1EA]/55">
<label htmlFor="batch-new-dest" className="mb-1 block label-muted">
Replace destination with
</label>
<input
@@ -146,7 +146,7 @@ export default function BatchEditPanel() {
value={newDestination}
onChange={(e) => { setNewDestination(e.target.value); setApplied(null); }}
placeholder="e.g. Universitätsplatz 3, 8010 Graz"
className="brand-input px-3 py-2 text-sm"
className="brand-input text-sm"
/>
</div>
<Button
+2 -2
View File
@@ -54,8 +54,8 @@ const CalendarPanel: React.FC<CalendarPanelProps> = ({ className = "" }) => {
}`;
return (
<div className={`brand-panel overflow-hidden rounded-2xl ${className}`}>
<div className="border-b border-white/10 p-4">
<div className={`brand-panel overflow-hidden ${className}`}>
<div className="section-header">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-white">Import Calendar</h3>
</div>
+3 -3
View File
@@ -40,14 +40,14 @@ const CalendarView: React.FC<CalendarViewProps> = ({ events, onDateSelect, selec
<div className="mb-4 flex items-center justify-between">
<button
onClick={() => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1))}
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/[0.05] text-[#F4F1EA]/76 transition-colors hover:border-[#D946EF]/45 hover:text-white"
className="icon-btn"
>
&lt;
</button>
<h2 className="text-xl font-semibold text-white">{format(currentDate, "MMMM yyyy")}</h2>
<button
onClick={() => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1))}
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/[0.05] text-[#F4F1EA]/76 transition-colors hover:border-[#D946EF]/45 hover:text-white"
className="icon-btn"
>
&gt;
</button>
@@ -140,7 +140,7 @@ const CalendarView: React.FC<CalendarViewProps> = ({ events, onDateSelect, selec
};
return (
<div className={`brand-panel rounded-2xl p-4 ${className}`}>
<div className={`brand-panel p-4 ${className}`}>
{renderHeader()}
{renderDays()}
{renderCells()}
+1 -1
View File
@@ -28,7 +28,7 @@ const DayEvents: React.FC<DayEventsProps> = ({ events, date, originStation, clas
if (filteredEvents.length === 0) {
return (
<div className={`brand-panel rounded-2xl px-4 py-8 text-center text-[#F4F1EA]/60 ${className}`}>
<div className={`brand-panel px-4 py-8 text-center text-[#F4F1EA]/60 ${className}`}>
<p>No events scheduled for {format(date, "MMMM d, yyyy")}</p>
</div>
);
+1 -1
View File
@@ -39,7 +39,7 @@ const UrlTab: React.FC<UrlTabProps> = ({ onLoadCalendar, loading, error, classNa
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com/calendar.ics"
className="brand-input px-3 py-2"
className="brand-input"
required
/>
</div>
+1 -1
View File
@@ -31,7 +31,7 @@ export default function CalendarPage() {
<div className="mx-auto max-w-6xl p-4 sm:p-6 lg:p-8">
<div className="mb-6 flex flex-col gap-4 rounded-2xl border border-white/10 bg-[#17112A]/55 p-5 shadow-[0_22px_70px_rgba(0,0,0,0.24)] backdrop-blur-xl sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.28em] text-brand-fuchsia">Calendar sync</p>
<p className="mb-2 eyebrow">Calendar sync</p>
<h1 className="text-3xl font-extrabold text-white sm:text-4xl">Calendar</h1>
<p className="mt-2 text-brand-light/66">View and manage every appointment from a single departure-focused calendar.</p>
</div>
+2 -2
View File
@@ -33,8 +33,8 @@ const BikeSection: React.FC<BikeSectionProps> = ({
}
return (
<div className={`mt-4 overflow-hidden rounded-xl border border-white/10 bg-black/16 ${className}`}>
<div className="border-b border-white/10 p-4">
<div className={`section-card mt-4 ${className}`}>
<div className="section-header">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-white">Bicycle Route</h3>
+13 -13
View File
@@ -128,24 +128,24 @@ export default function EventCard({ event, originStation }: EventCardProps) {
return (
<>
<div className="brand-panel overflow-hidden rounded-2xl p-4 sm:p-5">
<div className="brand-panel overflow-hidden 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-brand-fuchsia">Next stop</p>
<p className="mb-1 eyebrow">Next stop</p>
<h3 className="text-2xl font-bold text-white">{event.title}</h3>
</div>
<div className="flex items-start gap-2">
<CountdownBadge countdown={countdown} status={status} />
<button
onClick={() => setEditOpen(true)}
className="rounded-lg border border-white/10 bg-white/[0.05] px-2.5 py-1.5 text-xs font-medium text-[#F4F1EA]/60 transition-colors hover:border-brand-fuchsia/40 hover:text-white"
className="ghost-btn hover:border-brand-fuchsia/40 hover:text-white"
aria-label="Edit event"
>
Edit
</button>
<button
onClick={handleRemove}
className="rounded-lg border border-white/10 bg-white/[0.05] px-2.5 py-1.5 text-xs font-medium text-[#F4F1EA]/60 transition-colors hover:border-red-500/50 hover:text-red-400"
className="ghost-btn hover:border-red-500/50 hover:text-red-400"
aria-label="Remove event"
>
Remove
@@ -155,11 +155,11 @@ 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-brand-light/42">Destination</p>
<p className="label-muted">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-brand-light/42">Appointment</p>
<p className="label-muted">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>
@@ -183,7 +183,7 @@ 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-brand-pink/20 px-2 py-0.5 text-xs font-semibold text-pink-100">
<span className="badge-pill bg-brand-pink/20">
Active
</span>
)}
@@ -195,20 +195,20 @@ export default function EventCard({ event, originStation }: EventCardProps) {
<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-brand-light/42">Leave by</p>
<p className="mt-1 font-semibold text-white">
<p className="label-muted">Leave by</p>
<p className="stat-value">
{departureTime ? format(departureTime, "HH:mm") : "Pending"}
</p>
</div>
<div>
<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">
<p className="label-muted">Arrive by</p>
<p className="stat-value">
{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-brand-light/42">Buffer</p>
<p className="mt-1 font-semibold text-white">
<p className="label-muted">Buffer</p>
<p className="stat-value">
{arrivalBufferMinutes} min {calculatedMode ? `via ${calculatedMode}` : ""}
</p>
</div>
+1 -1
View File
@@ -92,7 +92,7 @@ const JourneyList: React.FC<JourneyListProps> = ({
{journey.cancelled ? "Cancelled" : journey.trains.join(", ")}
</span>
{index === 0 && (
<span className="ml-2 rounded-full bg-brand-fuchsia/20 px-2 py-0.5 text-xs font-semibold text-pink-100">
<span className="ml-2 badge-pill bg-brand-fuchsia/20">
Top
</span>
)}
+2 -2
View File
@@ -47,8 +47,8 @@ const TrainSection: React.FC<TrainSectionProps> = ({
walkError,
}) => {
return (
<div className={`overflow-hidden rounded-xl border border-white/10 bg-black/16 ${className}`}>
<div className="border-b border-white/10 p-4">
<div className={`section-card ${className}`}>
<div className="section-header">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-white">Trains</h3>
+2 -2
View File
@@ -25,8 +25,8 @@ const WalkingOption: React.FC<WalkingOptionProps> = ({ walkRoute, walkLoading, w
}
return (
<div className={`overflow-hidden rounded-xl border border-white/10 bg-black/16 ${className}`}>
<div className="border-b border-white/10 p-4">
<div className={`section-card ${className}`}>
<div className="section-header">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-white">Final Walk</h3>
@@ -72,7 +72,7 @@ export default function WienerLinienSection({
const stopDepartures = departuresByStop.get(stop.id) ?? [];
return (
<div key={stop.id} className="mb-4 rounded-xl border border-white/10 bg-black/16 p-4 last:mb-0">
<div key={stop.id} className="section-card mb-4 p-4 last:mb-0">
<h3 className="text-sm font-semibold text-white">{stop.name}</h3>
{stopDepartures.length === 0 ? (
+42
View File
@@ -66,6 +66,7 @@ body {
.brand-panel {
border: 1px solid rgba(244, 241, 234, 0.12);
border-radius: 1rem;
background:
linear-gradient(145deg, rgba(23, 17, 42, 0.92), rgba(11, 10, 24, 0.9)),
rgba(23, 17, 42, 0.88);
@@ -84,6 +85,7 @@ body {
border: 1px solid rgba(244, 241, 234, 0.14);
background: rgba(255, 255, 255, 0.06);
color: #F4F1EA;
padding: 0.5rem 0.75rem;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
@@ -98,3 +100,43 @@ body {
box-shadow: 0 0 0 3px rgba(217, 70, 239, 0.16);
background: rgba(255, 255, 255, 0.08);
}
/* ── Section containers ── */
.section-card {
@apply overflow-hidden rounded-xl border border-white/10 bg-black/16;
}
.section-header {
@apply border-b border-white/10 p-4;
}
/* ── Typography utilities ── */
.eyebrow {
@apply text-xs font-semibold uppercase tracking-[0.24em] text-brand-fuchsia;
}
.label-muted {
@apply text-xs font-semibold uppercase tracking-[0.16em] text-brand-light/42;
}
.stat-value {
@apply mt-1 font-semibold text-white;
}
/* ── Interactive elements ── */
.icon-btn {
@apply 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;
}
.ghost-btn {
@apply rounded-lg border border-white/10 bg-white/[0.05] px-2.5 py-1.5 text-xs font-medium text-brand-light/60 transition-colors;
}
/* ── Badges ── */
.badge-pill {
@apply rounded-full px-2 py-0.5 text-xs font-semibold text-pink-100;
}
+3 -3
View File
@@ -68,7 +68,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-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"
className="icon-btn"
>
{mounted && dark ? (
<svg aria-hidden="true" viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2">
@@ -84,7 +84,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-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"
className="icon-btn"
>
<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" />
@@ -97,7 +97,7 @@ const Header: React.FC<HeaderProps> = ({ className = "" }) => {
<AddEventModal isOpen={showAddEventModal} onClose={() => setShowAddEventModal(false)} />
{showSettingsModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 p-4 backdrop-blur-sm">
<div className="brand-panel relative w-full max-w-sm rounded-2xl p-6">
<div className="brand-panel relative w-full max-w-sm 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-brand-light/50 transition-colors hover:bg-white/10 hover:text-white"
+2 -2
View File
@@ -25,7 +25,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-brand-fuchsia">Departure desk</p>
<p className="mb-2 eyebrow">Departure desk</p>
<h1 className="max-w-2xl text-3xl font-extrabold leading-tight text-white sm:text-5xl">
Leave before the clock turns against you.
</h1>
@@ -43,7 +43,7 @@ export default function Home() {
</div>
</section>
{!nextEvent ? (
<div className="brand-panel mx-auto max-w-2xl rounded-2xl px-6 py-14 text-center">
<div className="brand-panel mx-auto max-w-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-brand-pink text-2xl font-black text-white shadow-[0_16px_44px_rgba(178,60,255,0.4)]">
T
</div>