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 ? (
+
+ ) : (
+
+ {initials}
+
+ )}
+
+
+
+
+
+
{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)