diff --git a/app/actions/add-event.ts b/app/actions/add-event.ts new file mode 100644 index 0000000..858cdf5 --- /dev/null +++ b/app/actions/add-event.ts @@ -0,0 +1,34 @@ +"use server"; +import { getInterestById } from "@/types/interest"; +import { createClient } from "@/utils/supabase/server"; +import { redirect } from "next/navigation"; + +export async function addEvent(formData: FormData) { + const supabase = await createClient(); + const data = await supabase.auth.getUser(); + + const date = formData.get("date") as string; + const place = formData.get("place") as string; + const mapsLink = formData.get("mapsLink") as string; + const interest = formData.get("interest") as string; + const description = formData.get("description") as string; + const user = data.data.user?.id; + + if (!user) { + console.error("User not authenticated"); + redirect("/error"); + } + + const interestFull = await getInterestById(interest as unknown as number); + + const { error } = await supabase + .from("events") + .insert([{ date, place, mapsLink, interest, description, user }]); + + if (error) { + console.error("Feil ved innsending:", error.message); + redirect("/error"); + } + + redirect(`/interests/${interestFull.slug}`); // Redirect to the interest page +} diff --git a/app/actions/add-interest.ts b/app/actions/add-interest.ts new file mode 100644 index 0000000..2eeceeb --- /dev/null +++ b/app/actions/add-interest.ts @@ -0,0 +1,32 @@ +"use server"; +import { createClient } from "@/utils/supabase/server"; +import { redirect } from "next/navigation"; + +export async function addInterest(formData: FormData) { + const supabase = await createClient(); + + const name = formData.get("name") as string; + const infinitiv = formData.get("infinitiv") as string; + const slug = generateSlug(name); + + const { error } = await supabase + .from("interests") + .insert([{ name, infinitiv, slug }]); + + if (error) { + console.error("Feil ved innsending:", error.message); + redirect("/error"); + } + + redirect("/interests"); +} + +function generateSlug(text: string) { + return text + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9 ]/g, "") + .trim() + .replace(/\s+/g, "-"); +} diff --git a/app/actions/comment.ts b/app/actions/comment.ts new file mode 100644 index 0000000..7bfd459 --- /dev/null +++ b/app/actions/comment.ts @@ -0,0 +1,39 @@ +"use server"; + +import { createClient } from "@/utils/supabase/server"; +import { redirect } from "next/navigation"; + +export async function addComment(formData: FormData) { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + const content = formData.get("content") as string; + const event_id = Number(formData.get("event_id")); + + if (!user) { + console.error("User not authenticated"); + redirect("/error"); + } + + if (!content || !event_id) { + console.error("Missing content or event_id"); + redirect("/error"); + } + + const { error } = await supabase.from("comments").insert([ + { + content, + event_id, + user_id: user.id, + }, + ]); + + if (error) { + console.error("Feil ved innsending av kommentar:", error.message); + redirect("/error"); + } + + redirect(`/events/${event_id}`); +} diff --git a/app/actions/event-attendance.ts b/app/actions/event-attendance.ts new file mode 100644 index 0000000..c17b2d3 --- /dev/null +++ b/app/actions/event-attendance.ts @@ -0,0 +1,48 @@ +// actions/handleAttendance.ts +"use server"; + +import { createClient } from "@/utils/supabase/server"; +import { redirect } from "next/navigation"; + +export async function handleAttendance(formData: FormData) { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + const event_id = Number(formData.get("event_id")); + const status = formData.get("status") as "skal" | "interessert"; + + if (!user) redirect("/auth/login"); + + const { data: existing } = await supabase + .from("event_attendance") + .select("id") + .eq("event_id", event_id) + .eq("user_id", user.id) + .single(); + + let error; + + if (existing) { + ({ error } = await supabase + .from("event_attendance") + .update({ status }) + .eq("id", existing.id)); + } else { + ({ error } = await supabase.from("event_attendance").insert([ + { + user_id: user.id, + event_id, + status, + }, + ])); + } + + if (error) { + console.error("Feil ved lagring:", error.message); + redirect("/error"); + } + + redirect(`/events/${event_id}`); +} diff --git a/app/actions/from-bergen.ts b/app/actions/from-bergen.ts new file mode 100644 index 0000000..cb08cd4 --- /dev/null +++ b/app/actions/from-bergen.ts @@ -0,0 +1,26 @@ +"use server"; + +import { createClient } from "@/utils/supabase/server"; +import { redirect } from "next/navigation"; +import { revalidatePath } from "next/cache"; + +export async function handleFromBergen(formData: FormData) { + const supabase = await createClient(); + + const user_id = formData.get("user_id") as string; + const is_from_bergen = formData.get("is_from_bergen") === "on"; // checkbox + + const { error } = await supabase + .from("profiles") + .update({ is_from_bergen }) + .eq("id", user_id); + + if (error) { + console.error("Feil ved oppdatering:", error.message); + redirect("/error"); + } + + // 💡 Refresh profilside + revalidatePath("/profile"); + redirect("/profile"); +} diff --git a/app/actions/interest-client.ts b/app/actions/interest-client.ts new file mode 100644 index 0000000..68ca086 --- /dev/null +++ b/app/actions/interest-client.ts @@ -0,0 +1,31 @@ +import { Interest } from "@/types/interest"; +import { createClient } from "@/utils/supabase/client"; + +export async function getInterestBySlugClient(slug: string): Promise { + const supabase = createClient(); + + const { data: interest, error } = await supabase + .from("interests") + .select("id, created_at, name, infinitiv, slug") + .eq("slug", slug) + .single(); + + if (!interest || error) + throw new Error(`Interest with slug "${slug}" not found`); + + return interest; +} + +export async function getInterestByIdClient(id: string): Promise { + const supabase = createClient(); + + const { data: interest, error } = await supabase + .from("interests") + .select("id, created_at, name, infinitiv, slug") + .eq("id", id) + .single(); + + if (!interest || error) throw new Error(`Interest with id "${id}" not found`); + + return interest; +} diff --git a/app/actions/login.ts b/app/actions/login.ts new file mode 100644 index 0000000..7b9f7fc --- /dev/null +++ b/app/actions/login.ts @@ -0,0 +1,32 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +import { createClient } from "@/utils/supabase/server"; + +export async function login(formData: FormData) { + const supabase = await createClient(); + + // type-casting here for convenience + // in practice, you should validate your inputs + const data = { + email: formData.get("email") as string, + password: formData.get("password") as string, + }; + + const { error } = await supabase.auth.signInWithPassword(data); + + if (error) { + redirect("/error"); + } + + if (data.email) { + console.log("Login successful, redirecting..."); + revalidatePath("/", "layout"); + redirect("/"); + } else { + console.warn("No user returned after login."); + redirect("/error"); + } +} diff --git a/app/actions/register.ts b/app/actions/register.ts new file mode 100644 index 0000000..466185e --- /dev/null +++ b/app/actions/register.ts @@ -0,0 +1,28 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +import { createClient } from "@/utils/supabase/server"; + +export async function register(formData: FormData) { + const supabase = await createClient(); + // type-casting here for convenience + // in practice, you should validate your inputs + const data = { + email: formData.get("email") as string, + password: formData.get("password") as string, + options: { + data: { + full_name: formData.get("fullName") as string, + username: formData.get("username") as string, + }, + }, + }; + const { error } = await supabase.auth.signUp(data); + if (error) { + redirect("/error"); + } + revalidatePath("/", "layout"); + redirect("/"); +} diff --git a/app/actions/signout.ts b/app/actions/signout.ts new file mode 100644 index 0000000..7fbf11a --- /dev/null +++ b/app/actions/signout.ts @@ -0,0 +1,21 @@ +"use server"; + +import { redirect } from "next/navigation"; + +import { createClient } from "@/utils/supabase/server"; + +export async function logout() { + try { + const supabase = await createClient(); + const { error } = await supabase.auth.signOut(); + if (error) { + console.error("Logout error:", error.message); + redirect("/error"); + return; + } + // Ingen direkte omdirigering her, vi lar klienten håndtere det + } catch (err) { + console.error("Unexpected error during logout:", err); + redirect("/error"); + } +} diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index b362ee8..0000000 --- a/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import NextAuth from "next-auth"; -import { authOptions } from "@lib/authOptions"; - -const handler = NextAuth(authOptions); - -export { handler as GET, handler as POST }; \ No newline at end of file diff --git a/app/api/interests/utils.ts b/app/api/interests/utils.ts deleted file mode 100644 index 0654816..0000000 --- a/app/api/interests/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { promises as fs } from "fs"; -import path from "path"; -import type { Interest } from "./../../../components/types/interests"; - -export async function getInterestBySlug( - slug: string -): Promise { - const filePath = path.join(process.cwd(), "public", "interests.json"); - const file = await fs.readFile(filePath, "utf-8"); - const interests: Interest[] = JSON.parse(file); - - return interests.find((interest) => interest.slug === slug); -} diff --git a/app/auth/confirm/route.ts b/app/auth/confirm/route.ts new file mode 100644 index 0000000..197ed06 --- /dev/null +++ b/app/auth/confirm/route.ts @@ -0,0 +1,35 @@ +import { type EmailOtpType } from "@supabase/supabase-js"; +import { type NextRequest, NextResponse } from "next/server"; + +import { createClient } from "@/utils/supabase/server"; + +// Creating a handler to a GET request to route /auth/confirm +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const token_hash = searchParams.get("token_hash"); + const type = searchParams.get("type") as EmailOtpType | null; + const next = "/account"; + + // Create redirect link without the secret token + const redirectTo = request.nextUrl.clone(); + redirectTo.pathname = next; + redirectTo.searchParams.delete("token_hash"); + redirectTo.searchParams.delete("type"); + + if (token_hash && type) { + const supabase = await createClient(); + + const { error } = await supabase.auth.verifyOtp({ + type, + token_hash, + }); + if (!error) { + redirectTo.searchParams.delete("next"); + return NextResponse.redirect(redirectTo); + } + } + + // return the user to an error page with some instructions + redirectTo.pathname = "/error"; + return NextResponse.redirect(redirectTo); +} diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx new file mode 100644 index 0000000..860cfee --- /dev/null +++ b/app/auth/login/page.tsx @@ -0,0 +1,41 @@ +import { login } from "@/app/actions/login"; +import Link from "next/link"; + +export default function LoginPage() { + return ( +
+
+

Logg inn

+ + + +
+

+ Er du ikke kompis enda?¿ {""} + + Registrer deg + +

+
+ ); +} diff --git a/app/auth/register/page.tsx b/app/auth/register/page.tsx new file mode 100644 index 0000000..a438521 --- /dev/null +++ b/app/auth/register/page.tsx @@ -0,0 +1,61 @@ +import { register } from "@/app/actions/register"; +import Link from "next/link"; + +export default function RegisterPage() { + return ( +
+
+

Registrer deg

+ + + + +
+ + +
+ +
+

+ Er du allerede kompis? {""} + + Logg inn + +

+
+ ); +} diff --git a/app/auth/signout/route.ts b/app/auth/signout/route.ts new file mode 100644 index 0000000..99ee3bf --- /dev/null +++ b/app/auth/signout/route.ts @@ -0,0 +1,21 @@ +import { createClient } from "@/utils/supabase/server"; +import { revalidatePath } from "next/cache"; +import { type NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + const supabase = await createClient(); + + // Check if a user's logged in + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (user) { + await supabase.auth.signOut(); + } + + revalidatePath("/", "layout"); + return NextResponse.redirect(new URL("/login", req.url), { + status: 302, + }); +} diff --git a/app/careers/[slug]/page.tsx b/app/careers/[slug]/page.tsx index ee929bc..5fc8f21 100644 --- a/app/careers/[slug]/page.tsx +++ b/app/careers/[slug]/page.tsx @@ -1,5 +1,5 @@ import { notFound } from "next/navigation"; -import LinkButton from "@/components/linkButton"; +import LinkButton from "@/components/link-button"; const jobOpenings = [ { diff --git a/app/careers/page.tsx b/app/careers/page.tsx index 29f69ff..2e50c65 100644 --- a/app/careers/page.tsx +++ b/app/careers/page.tsx @@ -1,4 +1,4 @@ -import LinkButton from "@/components/linkButton"; +import LinkButton from "@/components/link-button"; import React from "react"; const jobOpenings = [ diff --git a/app/error/page.tsx b/app/error/page.tsx new file mode 100644 index 0000000..f984f84 --- /dev/null +++ b/app/error/page.tsx @@ -0,0 +1,3 @@ +export default function ErrorPage() { + return

Sorry, something went wrong

; +} diff --git a/app/event/[slug]/page.tsx b/app/event/[slug]/page.tsx deleted file mode 100644 index 2100d72..0000000 --- a/app/event/[slug]/page.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client"; -import { formatDateWithDay, formatTime } from "@/components/lib/date-utils"; -import { Event, getEventById } from "@/components/types/post"; -import { Button } from "@/components/ui/button"; -import { Calendar, Clock, User } from "lucide-react"; -import { useSession } from "next-auth/react"; -import { usePathname } from "next/navigation"; -import { useState } from "react"; - -export default function EventPage() { - const pathname = usePathname(); - const id = pathname.split("/")[2]; - const event = getEventById(parseInt(id)); - - if (!event) return

Event not found

; - - return ( -
-

{event.place}

-
- -

{event.description}

-
- - -
- ); -} - -function EventInfo({ event }: { event: Event }) { - return ( -
-
- -

{formatDateWithDay(event.date)}

-
-
- -

{formatTime(event.date)}

-
-
- -

{event.user.name}

-
-
- ); -} - -function EventButtons() { - const [isGoing, setIsGoing] = useState(false); - const [isInterested, setIsInterested] = useState(false); - - const handleGoing = () => { - setIsGoing(!isGoing); - if (!isGoing) setIsInterested(false); - }; - - const handleInterested = () => { - setIsInterested(!isInterested); - if (!isInterested) setIsGoing(false); - }; - - return ( -
- - -
- ); -} - -type Comment = { - user: string; - text: string; - time: string; -}; - -function CommentSection() { - const [comments, setComments] = useState([]); - const [newComment, setNewComment] = useState(""); - const { data: session } = useSession(); - const user = session?.user; - - const handleAddComment = () => { - if (newComment.trim() === "" || !user) return; - - const newCommentObj: Comment = { - user: user.name || "Anonym", - text: newComment.trim(), - time: new Date().toLocaleString("no-NO", { - dateStyle: "short", - timeStyle: "short", - }), - }; - - setComments((prev) => [...prev, newCommentObj]); - setNewComment(""); - }; - - return ( -
-

Kommentarer

-
- {comments.map((comment, index) => ( -
-
-
- -
-
-

{comment.user}

-

{comment.time}

-
-
- -

{comment.text}

-
- ))} -
-
- setNewComment(e.target.value)} - /> - -
-
- ); -} diff --git a/app/events/[slug]/page.tsx b/app/events/[slug]/page.tsx new file mode 100644 index 0000000..efb76d5 --- /dev/null +++ b/app/events/[slug]/page.tsx @@ -0,0 +1,162 @@ +import { addComment } from "@/app/actions/comment"; +import { + formatDateTime, + formatDateWithDay, + formatTime, +} from "@/components/lib/date-utils"; +import { getEventById } from "@/types/event"; +import { type Event } from "@/types/event"; +import { type Comment } from "@/types/comment"; +import { getUserFromId } from "@/types/profile"; +import { createClient } from "@/utils/supabase/server"; +import { Calendar, Clock, User } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { handleAttendance } from "@/app/actions/event-attendance"; + +interface PageProps { + params: Promise<{ + slug: string; + }>; +} + +export default async function EventPage({ params }: PageProps) { + const { slug } = await params; + const event = await getEventById(parseInt(slug)); + return ( +
+

{event.place}

+
+ +

{event.description}

+
+ +
+ + +
+
+ ); +} + +async function EventInfo({ event }: { event: Event }) { + const user = await getUserFromId(event.user); + return ( +
+
+ +

{formatDateWithDay(event.date)}

+
+
+ +

{formatTime(event.date)}

+
+
+ +

{user.full_name}

+
+
+ ); +} + +async function EventButtons({ event }: { event: Event }) { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + const { data: existing } = await supabase + .from("event_attendance") + .select("status") + .eq("event_id", event.id) + .eq("user_id", user?.id) + .single(); + + const selectedStatus = existing?.status; + + return ( +
+ + + +
+ ); +} + +async function CommentSection({ eventId }: { eventId: number }) { + const supabase = await createClient(); + const { data: comments, error } = await supabase + .from("comments") + .select("*") + .eq("event_id", eventId) + .order("created_at", { ascending: false }); + + if (error) { + return

Kunne ikke laste kommentarer.

; + } + + return ( +
+

Kommentarer

+ {comments?.length === 0 ? ( +

Ingen kommentarer ennå.

+ ) : ( + comments?.map((comment) => ( + + )) + )} +
+ ); +} + +async function CommentComponent({ comment }: { comment: Comment }) { + const user = await getUserFromId(comment.user_id); + return ( +
+
+
+ +
+
+

{user.full_name}

+

+ {formatDateTime(comment.created_at)} +

+
+
+ +

{comment.content}

+
+ ); +} + +function CommentForm({ eventId }: { eventId: number }) { + return ( +
+ + + + + +
+ ); +} diff --git a/app/events/add/page.tsx b/app/events/add/page.tsx new file mode 100644 index 0000000..b674d3e --- /dev/null +++ b/app/events/add/page.tsx @@ -0,0 +1,87 @@ +import { addEvent } from "@/app/actions/add-event"; +import { getInterests } from "@/types/interest"; + +export default async function AddEvent() { + const interests = await getInterests(); + return ( +
+

Legg til arrangement

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +