Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 36 additions & 36 deletions frontend/app/components/AddEventForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

// --- Imports ---
import { useState } from "react";
import { EventType, Place } from "@/lib/types";
import { EventType, Place, RawEventWithLocation } from "@/lib/types";
import { EVENT_TYPES, TIME_OPTIONS } from "@/lib/constants";

// --- Component Props Interface ---
Expand All @@ -14,7 +14,7 @@ interface AddEventFormProps {
datePart: string;
timePart: string;
locationInput: string;
}) => Promise<void>;
}) => Promise<RawEventWithLocation | null>;
}

// --- Add Event Form Component ---
Expand Down Expand Up @@ -61,9 +61,9 @@ export default function AddEventForm({ places, onSubmit }: AddEventFormProps) {
return (
<div className="flex flex-col gap-3">
{/* Event Type Selection */}
<select
value={type}
onChange={e => setType(e.target.value as EventType)}
<select
value={type}
onChange={e => setType(e.target.value as EventType)}
className="bg-sky-950 text-sky-200 border border-sky-200 px-2 py-2 rounded w-full focus:border-blue-500 focus:outline-none appearance-none text-left"
style={{ backgroundImage: 'url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%23cbd5e1\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3e%3cpolyline points=\'6,9 12,15 18,9\'%3e%3c/polyline%3e%3c/svg%3e")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 8px center', backgroundSize: '16px' }}
>
Expand All @@ -73,46 +73,46 @@ export default function AddEventForm({ places, onSubmit }: AddEventFormProps) {
</option>
))}
</select>

{/* Event Description Input */}
<input
placeholder={type === "other" ? "Beskrivelse (påkrevd)" : "Beskrivelse (valgfritt)"}
value={description}
onChange={e => setDescription(e.target.value)}
className="bg-sky-950 text-sky-200 border border-sky-200 px-2 py-2 rounded w-full focus:border-blue-500 focus:outline-none placeholder-sky-200 text-left"
<input
placeholder={type === "other" ? "Beskrivelse (påkrevd)" : "Beskrivelse (valgfritt)"}
value={description}
onChange={e => setDescription(e.target.value)}
className="bg-sky-950 text-sky-200 border border-sky-200 px-2 py-2 rounded w-full focus:border-blue-500 focus:outline-none placeholder-sky-200 text-left"
required={type === "other"}
/>

{/* Date Selection */}
<input
type="date"
value={datePart}
onChange={e => setDatePart(e.target.value)}
className="bg-sky-950 text-sky-200 border border-sky-200 px-2 py-2 rounded w-full box-border focus:border-blue-500 focus:outline-none text-left"
style={{
WebkitAppearance: 'none',
<input
type="date"
value={datePart}
onChange={e => setDatePart(e.target.value)}
className="bg-sky-950 text-sky-200 border border-sky-200 px-2 py-2 rounded w-full box-border focus:border-blue-500 focus:outline-none text-left"
style={{
WebkitAppearance: 'none',
MozAppearance: 'textfield',
colorScheme: 'dark'
}}
/>

{/* Time Selection */}
<select
value={timePart}
onChange={e => setTimePart(e.target.value)}
<select
value={timePart}
onChange={e => setTimePart(e.target.value)}
className="bg-sky-950 text-sky-200 border border-sky-200 px-2 py-2 rounded w-full focus:border-blue-500 focus:outline-none appearance-none text-left"
style={{ backgroundImage: 'url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%23cbd5e1\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3e%3cpolyline points=\'6,9 12,15 18,9\'%3e%3c/polyline%3e%3c/svg%3e")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 8px center', backgroundSize: '16px' }}
>
{TIME_OPTIONS.map((time: string) => (
<option key={time} value={time} className="bg-gray-700 text-white">{time}</option>
))}
</select>

{/* Location Selection - Indoor/Gym Events */}
{(type === "boulder" || type === "toprope") ? (
<select
value={locationInput}
onChange={e => setLocationInput(e.target.value)}
<select
value={locationInput}
onChange={e => setLocationInput(e.target.value)}
className="bg-sky-950 text-sky-200 border border-sky-200 px-2 py-2 rounded w-full focus:border-blue-500 focus:outline-none appearance-none text-left"
style={{ backgroundImage: 'url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%23cbd5e1\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3e%3cpolyline points=\'6,9 12,15 18,9\'%3e%3c/polyline%3e%3c/svg%3e")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 8px center', backgroundSize: '16px' }}
>
Expand All @@ -123,18 +123,18 @@ export default function AddEventForm({ places, onSubmit }: AddEventFormProps) {
</select>
) : (
/* Location Input - Outdoor Events */
<input
type="text"
placeholder="Skriv inn sted"
value={locationInput}
onChange={e => setLocationInput(e.target.value)}
className="bg-sky-950 text-sky-200 border border-sky-200 px-2 py-2 rounded w-full focus:border-blue-500 focus:outline-none placeholder-gray-400 text-left"
<input
type="text"
placeholder="Skriv inn sted"
value={locationInput}
onChange={e => setLocationInput(e.target.value)}
className="bg-sky-950 text-sky-200 border border-sky-200 px-2 py-2 rounded w-full focus:border-blue-500 focus:outline-none placeholder-gray-400 text-left"
/>
)}

{/* Action Buttons */}
<button
onClick={handleSubmit}
<button
onClick={handleSubmit}
className="bg-blue-700 text-white px-4 py-2 rounded hover:bg-blue-800 mt-2"
>
Legg til
Expand Down
110 changes: 74 additions & 36 deletions frontend/app/components/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,16 @@ import { useEvents } from "@/hooks/useEvents";
import { useAuth } from "@/hooks/useAuth";
import { useEventModal } from "@/hooks/useEventModal";
import { useEventActions } from "@/hooks/useEventActions";
import { Place, User, RawEvent, RawEventWithLocation } from "@/lib/types";
import { Place, User, RawEvent, RawEventWithLocation, EventType } from "@/lib/types";
import { db } from "@/lib/firebase";
import { onSnapshot, collection, DocumentReference, getDocs } from "firebase/firestore";
import Modal from "./Modal";
import EventList from "./EventList";
import AddEventForm from "./AddEventForm";
import EventDetails from "./EventDetails";
import PostEventCreationModal from "./PostEventCreationModal";
import { fetchName, fetchPlaceName } from "@/lib/utils/firestoreUtils";

// --- Helper Functions ---
const getEventTypeLabel = (type: string): string => {
const typeLabels: Record<string, string> = {
// English variations
boulder: "Buldring",
toprope: "Tauklatring",
outdoor: "Utendørs",
other: "Annet",
// Norwegian variations (in case they're stored in Norwegian)
buldring: "Buldring",
tauklatring: "Tauklatring",
utendørs: "Utendørs",
annet: "Annet"
};
return typeLabels[type.toLowerCase()] || type;
};
import { getEventTypeLabel } from '../../lib/utils/eventTypes';

// --- Main Calendar Component ---
export default function Calendar() {
Expand All @@ -53,7 +38,7 @@ export default function Calendar() {
// Explicit type for selectedEvent to ensure correct typing
const typedSelectedEvent: RawEventWithLocation | null = selectedEvent as RawEventWithLocation | null;

const { handleJoinLeaveToggle, handleAddEvent, handleDeleteEvent } = useEventActions({
const { handleJoinLeaveToggle, handleAddEvent, handleDeleteEvent, handleShareToDiscord } = useEventActions({
user,
selectedEvent,
fetchEvents,
Expand All @@ -64,6 +49,41 @@ export default function Calendar() {
// --- Local State ---
const [places, setPlaces] = useState<Place[]>([]);
const [eventsWithNames, setEventsWithNames] = useState<RawEventWithLocation[]>([]);
const [postCreationModalOpen, setPostCreationModalOpen] = useState(false);
const [createdEvent, setCreatedEvent] = useState<RawEventWithLocation | null>(null);

// --- Handle Event Creation with Post-Creation Modal ---
const handleEventCreation = async (eventData: {
type: EventType;
description: string;
datePart: string;
timePart: string;
locationInput: string;
}) => {
const result = await handleAddEvent(eventData);
if (result) {
setCreatedEvent(result);
setPostCreationModalOpen(true);
closeEventModal(); // Close the creation modal
}
return result;
};

// --- Close Post-Creation Modal ---
const closePostCreationModal = () => {
setPostCreationModalOpen(false);
setCreatedEvent(null);
};

// --- Sync createdEvent with updated events data ---
useEffect(() => {
if (createdEvent && eventsWithNames.length > 0) {
const updatedEvent = eventsWithNames.find(event => event.id === createdEvent.id);
if (updatedEvent) {
setCreatedEvent(updatedEvent);
}
}
}, [eventsWithNames, createdEvent]);

// --- Fetch Available Places ---
useEffect(() => {
Expand All @@ -77,7 +97,7 @@ export default function Calendar() {

// --- Real-time Event Subscription with Data Enrichment ---
useEffect(() => {
if (!window || places.length === 0 || !db) return; // Wait for places to be loaded
if (!window || places.length === 0 || !db) return;

const unsubscribe = onSnapshot(collection(db, "events"), (snapshot) => {
// --- Map Raw Firestore Documents to CliffiEvent Format ---
Expand All @@ -89,6 +109,7 @@ export default function Calendar() {
createdBy: DocumentReference;
location: string | DocumentReference;
participants?: DocumentReference[];
sharedToDiscord?: boolean;
};

const dateStr = typeof raw.date === "string" ? raw.date : new Date().toISOString();
Expand All @@ -100,7 +121,8 @@ export default function Calendar() {
date: dateStr,
createdBy: raw.createdBy,
location: raw.location,
participants: raw.participants || []
participants: raw.participants || [],
sharedToDiscord: raw.sharedToDiscord || false
};
});

Expand All @@ -116,15 +138,15 @@ export default function Calendar() {
if (typeof ev.createdBy !== "string") {
const ref = ev.createdBy as DocumentReference<User>;
const cachedName = newNamesCache[ref.path];

// Don't use cached value if it looks like a UID (deleted user)
if (cachedName && cachedName !== ref.id) {
creatorName = cachedName;
} else {
creatorName = await fetchName(ref);
newNamesCache[ref.path] = creatorName;
}

creatorId = ref.id;
} else {
creatorName = ev.createdBy;
Expand All @@ -151,7 +173,7 @@ export default function Calendar() {
ev.participants.map(async (participant) => {
const ref = participant as DocumentReference<User>;
const cachedName = newNamesCache[ref.path];

let displayName: string;
// Don't use cached value if it looks like a UID (deleted user)
if (cachedName && cachedName !== ref.id) {
Expand All @@ -160,7 +182,7 @@ export default function Calendar() {
displayName = await fetchName(ref);
newNamesCache[ref.path] = displayName;
}

return { id: ref.id, displayName };
})
);
Expand Down Expand Up @@ -189,7 +211,8 @@ export default function Calendar() {
});

return () => unsubscribe();
}, [places.length]); // eslint-disable-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [places.length]);

// --- Loading State ---
if (loading) return <div className="text-center text-sky-200">Laster events...</div>;
Expand All @@ -212,7 +235,7 @@ export default function Calendar() {
<h2 className="text-center text-6xl font-bold mb-8 text-sky-200 tracking-widest uppercase drop-shadow-sm font-sans">
Cliffi
</h2>

{/* Upcoming Events Section */}
<EventList
events={upcomingEvents}
Expand Down Expand Up @@ -243,8 +266,8 @@ export default function Calendar() {

{/* Event Modal */}
{isEventModalOpen && (
<Modal
isOpen={isEventModalOpen}
<Modal
isOpen={isEventModalOpen}
onClose={closeEventModal}
title={typedSelectedEvent ? getEventTypeLabel(typedSelectedEvent.type) : (isAddingEvent ? "Ny økt" : undefined)}
>
Expand All @@ -253,18 +276,33 @@ export default function Calendar() {
) : isAddingEvent ? (
<AddEventForm
places={places}
onSubmit={handleAddEvent}
onSubmit={handleEventCreation}
/>
) : typedSelectedEvent ? (
<EventDetails
event={typedSelectedEvent}
user={user}
onJoinLeave={handleJoinLeaveToggle}
onDelete={handleDeleteEvent}
/>
(() => {
// Get the latest event data from the real-time updated list
const latestEvent = eventsWithNames.find(e => e.id === typedSelectedEvent.id) || typedSelectedEvent;
return (
<EventDetails
event={latestEvent}
user={user}
onJoinLeave={handleJoinLeaveToggle}
onDelete={handleDeleteEvent}
onShareToDiscord={handleShareToDiscord}
/>
);
})()
) : null}
</Modal>
)}

{/* Post Event Creation Modal */}
<PostEventCreationModal
event={createdEvent}
isOpen={postCreationModalOpen}
onClose={closePostCreationModal}
onRefresh={fetchEvents}
/>
</div>
);
}
Loading
Loading