diff --git a/client/public/frame.svg b/client/public/frame.svg new file mode 100644 index 0000000..2486653 --- /dev/null +++ b/client/public/frame.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/client/src/components/main/MemberProfile.tsx b/client/src/components/main/MemberProfile.tsx new file mode 100644 index 0000000..8d25ffc --- /dev/null +++ b/client/src/components/main/MemberProfile.tsx @@ -0,0 +1,94 @@ +"use client"; + +import Image from "next/image"; + +// unused atm, as the member isnt linked a project on the backend +/* export type MemberProfileProject = { + id: string; + name: string; + description?: string; + href?: string; +}; */ + +export type MemberProfileData = { + name: string; + about: string; + pronouns?: string; + profile_picture?: string; +}; + +type MemberProfileProps = { + member: MemberProfileData; + //projects?: MemberProfileProject[]; +}; + +function initialsFromName(name: string) { + return name + .trim() + .split(/\s+/) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase()) + .join(""); +} + +export function MemberProfile({ member }: MemberProfileProps) { + const initials = initialsFromName(member.name); + + return ( + <> +
+
+
+
+ {member.profile_picture ? ( + {`${member.name} + ) : ( +
+ {initials} +
+ )} +
+ golden pixel art frame around profile picture +
+
+
+

{member.name}

+
+
+

{member.pronouns}

+

{member.about}

+
+
+
+ {/* Template for Projects section */} +
+

Projects

+
+ {/* Div below is a single project card */} +
+
+ {/* Image and/or Link to Project */} +
+

+ {/* Project Title */} +

+

+ {/* Project description */} +

+
+
+
+ + ); +} diff --git a/client/src/hooks/useMember.ts b/client/src/hooks/useMember.ts new file mode 100644 index 0000000..5e4ac18 --- /dev/null +++ b/client/src/hooks/useMember.ts @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; + +import api from "@/lib/api"; + +type ApiMember = { + name: string; + about: string; + pronouns?: string; + profile_picture?: string; +}; + +// return api member, import id number from router, is not enabled if not a number type +export const useMember = (id?: number) => { + return useQuery({ + queryKey: ["member", id], + queryFn: async () => { + const response = await api.get(`/members/${id}/`); + return response.data; + }, + enabled: Number.isFinite(id), + }); +}; diff --git a/client/src/pages/members/[id].tsx b/client/src/pages/members/[id].tsx new file mode 100644 index 0000000..f3bf50f --- /dev/null +++ b/client/src/pages/members/[id].tsx @@ -0,0 +1,40 @@ +import { useRouter } from "next/router"; + +import { useMember } from "@/hooks/useMember"; + +import { MemberProfile } from "../../components/main/MemberProfile"; + +// hook assumes correct input, page sanitises to correct type +function normaliseId(id: string | string[] | number | undefined) { + if (typeof id === "number" && Number.isFinite(id)) { + return id; + } + + if (typeof id === "string") { + const parsed = Number(id); + return Number.isFinite(parsed) ? parsed : undefined; + } + + return undefined; +} + +export default function MemberPage() { + const router = useRouter(); + const id = normaliseId(router.query.id); + + const { + data: member, + isPending, + isError, + } = useMember(router.isReady ? id : undefined); + + if (isPending) { + return null; + } + + if (isError || !member) { + return

Member not found

; + } + + return ; +} diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 0382bf6..e644507 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -16,6 +16,7 @@ --light-1: hsl(0 0% 100%); --light-2: hsl(236.25, 96%, 90.2%); + --light-3: hsl(235, 96%, 80%); --light-alt: hsl(183 100% 79%); --logo-blue-1: hsl(236 62% 95%); diff --git a/server/game_dev/migrations/0005_alter_member_profile_picture.py b/server/game_dev/migrations/0005_alter_member_profile_picture.py new file mode 100644 index 0000000..76031d2 --- /dev/null +++ b/server/game_dev/migrations/0005_alter_member_profile_picture.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.15 on 2026-01-18 15:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0004_alter_event_date"), + ] + + operations = [ + migrations.AlterField( + model_name="member", + name="profile_picture", + field=models.ImageField(blank=True, null=True, upload_to="profiles/"), + ), + ] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 6398070..af1e12a 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -4,7 +4,7 @@ class Member(models.Model): name = models.CharField(max_length=200) active = models.BooleanField(default=True) - profile_picture = models.ImageField(upload_to="profiles/", null=True) + profile_picture = models.ImageField(upload_to="profiles/", null=True, blank=True) about = models.CharField(max_length=256, blank=True) pronouns = models.CharField(max_length=20, blank=True) diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index b50638d..57b8c22 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -23,5 +23,5 @@ class Meta: "name", "profile_picture", "about", - "pronouns" + "pronouns", ] diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index d42d6a8..f503749 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,7 +1,8 @@ from django.urls import path -from .views import EventListAPIView, EventDetailAPIView +from .views import EventListAPIView, EventDetailAPIView, MemberAPIView urlpatterns = [ path("events/", EventListAPIView.as_view(), name="events-list"), path("events//", EventDetailAPIView.as_view()), + path('members//', MemberAPIView.as_view()) ] diff --git a/server/game_dev/views.py b/server/game_dev/views.py index 89ce3a9..20e4a92 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -2,8 +2,8 @@ # Create your views here. from rest_framework import generics -from .models import Event -from .serializers import EventSerializer +from .models import Event, Member +from .serializers import EventSerializer, MemberSerializer from django.utils import timezone from rest_framework.exceptions import ValidationError from rest_framework.pagination import PageNumberPagination @@ -51,3 +51,11 @@ class EventDetailAPIView(generics.RetrieveAPIView): def get_queryset(self): return Event.objects.filter(id=self.kwargs["id"]) + + +class MemberAPIView(generics.RetrieveAPIView): + serializer_class = MemberSerializer + lookup_field = "id" + + def get_queryset(self): + return Member.objects.filter(active=True)