Skip to content

Commit ea02e76

Browse files
committed
Improve mobile navigation, article listing, events filters, and ISR publishing path
1 parent 20790ab commit ea02e76

26 files changed

Lines changed: 1098 additions & 529 deletions

File tree

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
'use server'
1+
"use server";
22

3+
import { revalidatePostContent } from "@/lib/revalidate-public";
34
import { createClient } from "@/lib/supabase/server";
45
import type { JSONContent } from "novel";
56

67
export async function updatePost(postId: string, title: string, content: JSONContent) {
7-
const supabase = await createClient();
8-
const { error } = await supabase.from('posts').update({ title, content }).eq('id', postId);
9-
return error;
10-
}
8+
const supabase = await createClient();
9+
const { data: post, error } = await supabase
10+
.from("posts")
11+
.update({ title, content })
12+
.eq("id", postId)
13+
.select("id, slug")
14+
.single();
15+
if (!error && post) {
16+
revalidatePostContent(post);
17+
}
18+
return error;
19+
}
Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,46 @@
1-
'use server'
1+
"use server";
22

3+
import { revalidatePostContent } from "@/lib/revalidate-public";
34
import { assertAuthenticated } from "@/lib/supabase/authentication";
45
import { createClient } from "@/lib/supabase/server";
5-
import { revalidatePath } from "next/cache";
6-
import { redirect } from "next/navigation";
76
import type { JSONContent } from "novel";
87

98
async function createPost(title: string, content: JSONContent, isPublic: boolean) {
10-
const supabase = await createClient();
11-
const user = await assertAuthenticated(supabase);
12-
13-
// Get the author_id for the current user, fallback to system author
14-
const { data: authorData } = await supabase
15-
.from('authors')
16-
.select('id')
17-
.eq('user_id', user.id)
18-
.single();
19-
20-
const author_id = authorData?.id || '8a3ac70b-7f88-4767-b88a-0645bfdaf817';
21-
22-
const { data: post, error } = await supabase
23-
.from('posts')
24-
.insert([
25-
{
26-
title,
27-
content,
28-
is_public: isPublic,
29-
created_by: user.id,
30-
author_id
31-
}
32-
])
33-
.select()
34-
.single();
35-
36-
if (error) {
37-
console.error('Error creating post:', error);
38-
throw new Error(error.message);
39-
}
40-
41-
revalidatePath('/', 'page');
42-
return post;
9+
const supabase = await createClient();
10+
const user = await assertAuthenticated(supabase);
11+
12+
// Get the author_id for the current user, fallback to system author
13+
const { data: authorData } = await supabase.from("authors").select("id").eq("user_id", user.id).single();
14+
15+
const author_id = authorData?.id || "8a3ac70b-7f88-4767-b88a-0645bfdaf817";
16+
17+
const { data: post, error } = await supabase
18+
.from("posts")
19+
.insert([
20+
{
21+
title,
22+
content,
23+
is_public: isPublic,
24+
created_by: user.id,
25+
author_id,
26+
},
27+
])
28+
.select()
29+
.single();
30+
31+
if (error) {
32+
console.error("Error creating post:", error);
33+
throw new Error(error.message);
34+
}
35+
36+
revalidatePostContent({ id: post.id, slug: post.slug });
37+
return post;
4338
}
4439

4540
export async function publishPost(title: string, content: JSONContent) {
46-
return await createPost(title, content, true);
41+
return await createPost(title, content, true);
4742
}
4843

4944
export async function saveDraft(title: string, content: JSONContent) {
50-
return await createPost(title, content, false);
51-
}
45+
return await createPost(title, content, false);
46+
}

app/(main)/events/EventsPageClient.tsx

Lines changed: 89 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,61 @@ import posthog from "posthog-js";
1010
import { useEffect, useState } from "react";
1111
import { toast } from "sonner";
1212

13-
function formatShortDate(date: string | Date) {
13+
const EVENTS_TIMEZONE = "Europe/Lisbon";
14+
15+
function getDayKey(date: string | Date) {
16+
return new Intl.DateTimeFormat("en-CA", {
17+
timeZone: EVENTS_TIMEZONE,
18+
year: "numeric",
19+
month: "2-digit",
20+
day: "2-digit",
21+
}).format(new Date(date));
22+
}
23+
24+
function getRelativeDayLabel(date: string | Date) {
25+
const targetKey = getDayKey(date);
26+
const now = new Date();
27+
const todayKey = getDayKey(now);
28+
const tomorrowKey = getDayKey(new Date(now.getTime() + 24 * 60 * 60 * 1000));
29+
30+
if (targetKey === todayKey) {
31+
return "Today";
32+
}
33+
34+
if (targetKey === tomorrowKey) {
35+
return "Tomorrow";
36+
}
37+
38+
return null;
39+
}
40+
41+
function formatAbsoluteEventDate(date: string | Date) {
1442
const d = new Date(date);
1543
return d.toLocaleDateString("en-US", {
16-
timeZone: "Europe/Lisbon",
44+
timeZone: EVENTS_TIMEZONE,
1745
day: "numeric",
1846
month: "long",
1947
weekday: "long",
2048
});
2149
}
2250

51+
function formatEventDate(date: string | Date, withRelativeLabels = true) {
52+
if (!withRelativeLabels) {
53+
return formatAbsoluteEventDate(date);
54+
}
55+
56+
const relativeDayLabel = getRelativeDayLabel(date);
57+
58+
if (relativeDayLabel) {
59+
return relativeDayLabel;
60+
}
61+
62+
return formatAbsoluteEventDate(date);
63+
}
64+
2365
// Helper function to check if two dates are on the same day
2466
function isSameDay(date1: Date, date2: Date) {
25-
return (
26-
date1.getFullYear() === date2.getFullYear() &&
27-
date1.getMonth() === date2.getMonth() &&
28-
date1.getDate() === date2.getDate()
29-
);
67+
return getDayKey(date1) === getDayKey(date2);
3068
}
3169

3270
interface Event {
@@ -41,16 +79,19 @@ interface Event {
4179

4280
interface EventsPageProps {
4381
initialEvents: Event[];
44-
city?: string; // Optional city prop
45-
user?: { id: string; role: string } | null; // Add user prop
4682
}
4783

48-
export default function EventsPageClient({ initialEvents, city, user }: EventsPageProps) {
84+
function formatCityLabel(city: string) {
85+
return city.charAt(0).toUpperCase() + city.slice(1);
86+
}
87+
88+
export default function EventsPageClient({ initialEvents }: EventsPageProps) {
4989
const [events, setEvents] = useState<Event[]>(initialEvents);
5090
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
5191
const [filteredEvents, setFilteredEvents] = useState<Event[]>(initialEvents);
5292
const [pageLoadTime] = useState(Date.now()); // ADD THIS
5393
const [hasClickedEvent, setHasClickedEvent] = useState(false); // ADD THIS
94+
const [hasHydrated, setHasHydrated] = useState(false);
5495
const router = useRouter();
5596

5697
// Filter for cities, different styles applied depending on the path.
@@ -65,15 +106,22 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
65106
setEvents(initialEvents);
66107
}, [initialEvents]);
67108

109+
useEffect(() => {
110+
setHasHydrated(true);
111+
}, []);
112+
68113
// Filter events when selectedDate changes
69114
useEffect(() => {
115+
const cityFilteredEvents =
116+
cityParam === "all" ? events : events.filter((event) => event.city.trim().toLowerCase() === cityParam);
117+
70118
if (selectedDate) {
71-
const eventsForDate = events.filter((event) => isSameDay(new Date(event.start_time), selectedDate));
72-
setFilteredEvents(eventsForDate);
73-
} else {
74-
setFilteredEvents(events);
119+
setFilteredEvents(cityFilteredEvents.filter((event) => isSameDay(new Date(event.start_time), selectedDate)));
120+
return;
75121
}
76-
}, [selectedDate, events]);
122+
123+
setFilteredEvents(cityFilteredEvents);
124+
}, [cityParam, selectedDate, events]);
77125

78126
// Track when events listing is loaded/changed
79127
useEffect(() => {
@@ -116,6 +164,14 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
116164
router.push(city === "all" ? "/events" : `/events?city=${city}`);
117165
};
118166

167+
const cityFilterButtonClass = (city: string) =>
168+
cn(
169+
"rounded-full border border-transparent bg-neutral-100 text-muted-foreground transition-[background-color,border-color,color,box-shadow,transform] duration-150 ease hover:shadow-sm motion-reduce:transition-none motion-safe:active:scale-[0.98] dark:bg-[rgba(10,46,61,0.78)] dark:text-[rgba(158,210,225,0.82)] dark:hover:bg-[rgba(4,201,216,0.12)] dark:hover:text-[#4ce4f0]",
170+
cityParam === city
171+
? "bg-[#dff6f7] text-[#28aeb8] hover:bg-[#dff6f7] hover:text-[#28aeb8] dark:border-[rgba(76,228,240,0.4)] dark:bg-[rgba(4,201,216,0.18)] dark:text-[#4ce4f0] dark:hover:bg-[rgba(4,201,216,0.18)] dark:hover:text-[#4ce4f0]"
172+
: "text-neutral-600 hover:text-neutral-900",
173+
);
174+
119175
// Handle calendar date click
120176
const handleDateClick = (date: Date) => {
121177
// Check if the clicked date has events
@@ -160,13 +216,13 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
160216

161217
return (
162218
<div className="space-y-8 md:p-4 animate-in">
163-
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
164-
<h1 className="text-2xl font-bold leading-tight [text-wrap:pretty]">
219+
<div className="flex flex-col gap-3 pb-4 pt-2 sm:flex-row sm:items-start sm:justify-between md:pb-0">
220+
<h1 className="text-2xl font-extrabold tracking-tight leading-tight text-[#104357] [text-wrap:pretty] dark:text-[#E3F2F7]">
165221
{selectedDate
166-
? `Events for ${formatShortDate(selectedDate)}`
167-
: city
168-
? `Upcoming Events in ${city.charAt(0).toUpperCase() + city.slice(1)}`
169-
: "Upcoming Events"}
222+
? `Events for ${formatEventDate(selectedDate, hasHydrated)}`
223+
: cityParam !== "all"
224+
? `Events in ${formatCityLabel(cityParam)}`
225+
: "Events"}
170226
</h1>
171227

172228
{selectedDate && (
@@ -182,10 +238,12 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
182238

183239
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
184240
{/* Events List - Takes up 2/3 of the space on large screens */}
185-
<div className="space-y-4 lg:col-span-2">
241+
<div className="order-2 space-y-4 lg:order-1 lg:col-span-2">
186242
{filteredEvents.length === 0 ? (
187243
<div className="rounded-md border border-dashed px-6 py-10 text-center text-base leading-relaxed text-muted-foreground">
188-
{selectedDate ? `No events found for ${formatShortDate(selectedDate)}` : "No upcoming events found"}
244+
{selectedDate
245+
? `No events found for ${formatEventDate(selectedDate, hasHydrated)}`
246+
: "No upcoming events found"}
189247
</div>
190248
) : (
191249
filteredEvents?.map((event) => {
@@ -197,8 +255,8 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
197255
<div key={event.id} className="space-y-4">
198256
{showDateHeading && (
199257
<>
200-
<h2 className="mt-6 text-lg font-semibold text-muted-foreground">
201-
{formatShortDate(event.start_time)}
258+
<h2 className="mt-6 text-lg font-semibold text-[#104357] dark:text-[#E3F2F7]">
259+
{formatEventDate(event.start_time, hasHydrated)}
202260
</h2>
203261
</>
204262
)}
@@ -218,7 +276,6 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
218276
});
219277
}}
220278
onDelete={handleDeleteEvent}
221-
isAdmin={user?.role === "admin" || process.env.NEXT_ALLOW_BAD_UI === "true"}
222279
/>
223280
</div>
224281
);
@@ -227,8 +284,8 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
227284
</div>
228285

229286
{/* Calendar Sidebar - Takes up 1/3 of the space on large screens */}
230-
<div className="lg:col-span-1">
231-
<div className="sticky top-4 flex flex-col gap-10">
287+
<div className="order-1 lg:order-2 lg:col-span-1">
288+
<div className="flex flex-col gap-6 lg:sticky lg:top-4 lg:gap-10">
232289
<section id="city_filters" className="flex flex-col gap-3">
233290
<h2 className="text-sm font-semibold leading-5 text-[#104357] dark:text-[#E3F2F7]">Events by City</h2>
234291

@@ -240,12 +297,7 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
240297
handleFilterClick("all");
241298
}}
242299
variant="outline"
243-
className={cn(
244-
"rounded-full border-none bg-neutral-100 text-muted-foreground transition-[background-color,color,box-shadow,transform] duration-150 ease hover:shadow-sm motion-reduce:transition-none motion-safe:active:scale-[0.98]",
245-
cityParam === "all"
246-
? "bg-[#dff6f7] text-[#28aeb8] hover:bg-[#dff6f7] hover:text-[#28aeb8]"
247-
: "text-neutral-600 hover:text-neutral-900",
248-
)}
300+
className={cityFilterButtonClass("all")}
249301
>
250302
<Link href="/events">Everything</Link>
251303
</Button>
@@ -256,10 +308,7 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
256308
handleFilterClick("lisboa");
257309
}}
258310
variant="outline"
259-
className={cn(
260-
"rounded-full border-none bg-neutral-100 text-muted-foreground transition-[background-color,color,box-shadow,transform] duration-150 ease hover:shadow-sm motion-reduce:transition-none motion-safe:active:scale-[0.98]",
261-
cityParam === "lisboa" && "bg-[#dff6f7] text-[#28aeb8] hover:bg-[#dff6f7] hover:text-[#28aeb8]",
262-
)}
311+
className={cityFilterButtonClass("lisboa")}
263312
>
264313
<Link href="/events?city=lisboa">Lisboa</Link>
265314
</Button>
@@ -270,10 +319,7 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
270319
handleFilterClick("porto");
271320
}}
272321
variant="outline"
273-
className={cn(
274-
"rounded-full border-none bg-neutral-100 text-muted-foreground transition-[background-color,color,box-shadow,transform] duration-150 ease hover:shadow-sm motion-reduce:transition-none motion-safe:active:scale-[0.98]",
275-
cityParam === "porto" && "bg-[#dff6f7] text-[#28aeb8] hover:bg-[#dff6f7] hover:text-[#28aeb8]",
276-
)}
322+
className={cityFilterButtonClass("porto")}
277323
>
278324
<Link href="/events?city=porto">Porto</Link>
279325
</Button>
@@ -284,10 +330,7 @@ export default function EventsPageClient({ initialEvents, city, user }: EventsPa
284330
handleFilterClick("online");
285331
}}
286332
variant="outline"
287-
className={cn(
288-
"rounded-full border-none bg-neutral-100 text-muted-foreground transition-[background-color,color,box-shadow,transform] duration-150 ease hover:shadow-sm motion-reduce:transition-none motion-safe:active:scale-[0.98]",
289-
cityParam === "online" && "bg-[#dff6f7] text-[#28aeb8] hover:bg-[#dff6f7] hover:text-[#28aeb8]",
290-
)}
333+
className={cityFilterButtonClass("online")}
291334
>
292335
<Link href="/events?city=online">Online</Link>
293336
</Button>

0 commit comments

Comments
 (0)