From 09363a71cf932f61ab90cac7e1013c381b17544b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Sosi=C5=84ski?= <164782094+mik0lajek@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:43:03 +0100 Subject: [PATCH 1/8] add: correct user login, registration and logout --- backend/config/settings.py | 29 +++++++- backend/config/urls.py | 4 +- backend/users/admin.py | 17 +++++ backend/users/migrations/0001_initial.py | 47 +++++++++++++ backend/users/migrations/__init__.py | 0 backend/users/models.py | 8 +++ backend/users/serializers.py | 13 ++++ backend/users/urls.py | 10 +++ backend/users/views/auth_views.py | 86 ++++++++++++++++++++++++ frontend/package-lock.json | 22 +----- 10 files changed, 212 insertions(+), 24 deletions(-) create mode 100644 backend/users/admin.py create mode 100644 backend/users/migrations/0001_initial.py create mode 100644 backend/users/migrations/__init__.py create mode 100644 backend/users/models.py create mode 100644 backend/users/serializers.py create mode 100644 backend/users/urls.py create mode 100644 backend/users/views/auth_views.py diff --git a/backend/config/settings.py b/backend/config/settings.py index e3b7b05..d42537e 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -25,8 +25,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] - +ALLOWED_HOSTS = ["*"] # Application definition @@ -37,9 +36,18 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + + 'rest_framework', + 'corsheaders', + 'django_extensions', + + 'users', ] +AUTH_USER_MODEL = 'users.User' + MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -49,6 +57,23 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +CORS_ALLOW_CREDENTIALS = True + +CORS_ALLOWED_ORIGINS = [ + "http://localhost:3000", # frontend +] + +CSRF_TRUSTED_ORIGINS = [ + "http://localhost:3000", +] + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.SessionAuthentication", + ], +} + + ROOT_URLCONF = 'config.urls' TEMPLATES = [ diff --git a/backend/config/urls.py b/backend/config/urls.py index c74036a..002ac7b 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -15,8 +15,10 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), + path("api/auth/", include("users.urls")), ] + diff --git a/backend/users/admin.py b/backend/users/admin.py new file mode 100644 index 0000000..1ce6cbe --- /dev/null +++ b/backend/users/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import User + +@admin.register(User) +class CustomUserAdmin(UserAdmin): + model = User + + list_display = ("username", "email", "avatar_url", "created_at", "is_staff") + + fieldsets = UserAdmin.fieldsets + ( + ("Profile info", {"fields": ("avatar_url", "bio")}), + ) + + add_fieldsets = UserAdmin.add_fieldsets + ( + ("Profile info", {"fields": ("avatar_url", "bio")}), + ) diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py new file mode 100644 index 0000000..3abcaae --- /dev/null +++ b/backend/users/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 6.0.3 on 2026-03-11 18:15 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True)), + ('avatar_url', models.URLField(blank=True, null=True)), + ('bio', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/backend/users/migrations/__init__.py b/backend/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/users/models.py b/backend/users/models.py new file mode 100644 index 0000000..c66ed3c --- /dev/null +++ b/backend/users/models.py @@ -0,0 +1,8 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models + +class User(AbstractUser): + email = models.EmailField(unique=True) + avatar_url = models.URLField(blank=True, null=True) + bio = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) \ No newline at end of file diff --git a/backend/users/serializers.py b/backend/users/serializers.py new file mode 100644 index 0000000..8016236 --- /dev/null +++ b/backend/users/serializers.py @@ -0,0 +1,13 @@ +from rest_framework import serializers +from .models import User + +class RegisterSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + + class Meta: + model = User + fields = ["username", "email", "password"] + + def create(self, validated_data): + user = User.objects.create_user(**validated_data) + return user \ No newline at end of file diff --git a/backend/users/urls.py b/backend/users/urls.py new file mode 100644 index 0000000..db2a1f7 --- /dev/null +++ b/backend/users/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from .views.auth_views import csrf_view, login_view, register_view, me_view, logout_view + +urlpatterns = [ + path("csrf/", csrf_view), + path("login/", login_view), + path("register/", register_view), + path("me/", me_view), + path("logout/", logout_view), +] diff --git a/backend/users/views/auth_views.py b/backend/users/views/auth_views.py new file mode 100644 index 0000000..bbab4e1 --- /dev/null +++ b/backend/users/views/auth_views.py @@ -0,0 +1,86 @@ +# LOGIN +from django.contrib.auth import authenticate, login, logout +from rest_framework.decorators import api_view +from rest_framework.response import Response +from django.views.decorators.csrf import csrf_protect + +@api_view(["POST"]) +@csrf_protect +def login_view(request): + username = request.data.get("username") + password = request.data.get("password") + + user = authenticate(username=username, password=password) + + if user is not None: + login(request, user) + return Response({ + "user": { + "username": user.username, + "email": user.email, + "avatar_url": user.avatar_url, + "bio": user.bio, + "created_at": user.created_at, + } + }) + + return Response({"error": "Invalid credentials"}, status=400) + + +# REGISTER +from ..serializers import RegisterSerializer + +@api_view(["POST"]) +@csrf_protect +def register_view(request): + serializer = RegisterSerializer(data=request.data) + + if serializer.is_valid(): + user = serializer.save() + return Response({ + "user": { + "username": user.username, + "email": user.email, + "avatar_url": user.avatar_url, + "bio": user.bio, + "created_at": user.created_at, + } + }, status=201) + + return Response({"error": serializer.errors}, status=400) + + +# CURRENT USER/ME +from rest_framework.permissions import IsAuthenticated +from rest_framework.decorators import permission_classes + +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def me_view(request): + user = request.user + return Response({ + "user": { + "username": user.username, + "email": user.email, + "avatar_url": user.avatar_url, + "bio": user.bio, + "created_at": user.created_at, + } + }) + + +# LOGOUT +@api_view(["POST"]) +def logout_view(request): + logout(request) + return Response({"message": "Logged out"}) + + +# CSRF +from django.middleware.csrf import get_token +from rest_framework.decorators import api_view +from rest_framework.response import Response + +@api_view(["GET"]) +def csrf_view(request): + return Response({"csrfToken": get_token(request)}) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0f679c1..e6553fb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -98,7 +98,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -619,7 +618,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1791,7 +1789,6 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -3858,7 +3855,6 @@ "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3869,7 +3865,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3880,7 +3875,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3942,7 +3936,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -4481,7 +4474,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5006,7 +4998,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6012,7 +6003,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6198,7 +6188,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6509,7 +6498,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7232,7 +7220,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -9393,7 +9380,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9649,7 +9635,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9659,7 +9644,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11139,8 +11123,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -11212,7 +11195,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11461,7 +11443,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11983,7 +11964,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 9dbabd0516912d30a2561288ed6b7560308d21ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Sosi=C5=84ski?= <164782094+mik0lajek@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:51:09 +0100 Subject: [PATCH 2/8] update requirements.txt --- backend/requirements.txt | Bin 246 -> 294 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 5abe49f3b5b9a916b0e13d92f1692a2e8b8f048d..e685de373020894a9a9ef60dccbfd0e687a9d91c 100644 GIT binary patch delta 41 rcmeyyxQuB+o4hVVDnkWB36RWVC}zlH$OqH547Lm=40;TP6H6Nb;s6Qe delta 9 QcmZ3+^o?;s+r(`J02L+#Z2$lO From c0794245f67d938cb594ff45be25e4bfb564d6a4 Mon Sep 17 00:00:00 2001 From: Zwolak Date: Tue, 17 Mar 2026 20:40:53 +0100 Subject: [PATCH 3/8] Adding profile and dashboard --- frontend/app/(auth)/login/page.tsx | 171 +++++++++--------- frontend/app/dashboard/layout.tsx | 30 +++ frontend/app/dashboard/page.tsx | 27 +-- .../app/dashboard/profile/ProfileAvatar.tsx | 50 +++++ .../dashboard/profile/ProfileDangerZone.tsx | 29 +++ .../app/dashboard/profile/ProfileDetails.tsx | 33 ++++ .../app/dashboard/profile/ProfileHeader.tsx | 8 + frontend/app/dashboard/profile/page.tsx | 15 ++ frontend/app/layout.tsx | 23 +-- .../components/dashboard/FullscreenLoader.tsx | 10 + frontend/components/dashboard/Sidebar.tsx | 107 +++++++++++ 11 files changed, 389 insertions(+), 114 deletions(-) create mode 100644 frontend/app/dashboard/layout.tsx create mode 100644 frontend/app/dashboard/profile/ProfileAvatar.tsx create mode 100644 frontend/app/dashboard/profile/ProfileDangerZone.tsx create mode 100644 frontend/app/dashboard/profile/ProfileDetails.tsx create mode 100644 frontend/app/dashboard/profile/ProfileHeader.tsx create mode 100644 frontend/app/dashboard/profile/page.tsx create mode 100644 frontend/components/dashboard/FullscreenLoader.tsx create mode 100644 frontend/components/dashboard/Sidebar.tsx diff --git a/frontend/app/(auth)/login/page.tsx b/frontend/app/(auth)/login/page.tsx index ccfcc65..32e9e87 100644 --- a/frontend/app/(auth)/login/page.tsx +++ b/frontend/app/(auth)/login/page.tsx @@ -1,102 +1,105 @@ "use client"; +import Link from "next/link"; import { useState } from "react"; +import { Home, User, Settings, LogOut } from "lucide-react"; +import { logout } from "@/lib/api"; import { useRouter } from "next/navigation"; -import { login, LoginData, ApiResponse, User } from "@/lib/api"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import LOGO from "../../../public/backlog-logo.png"; -import Image from "next/image"; -import { toastSuccess, toastError } from "@/lib/toast"; -import Link from "next/link"; -export default function LoginPage() { +export default function Sidebar() { + const [expanded, setExpanded] = useState(false); const router = useRouter(); - const [form, setForm] = useState({ username: "", password: "" }); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - try { - const data: ApiResponse = await login(form); - if (data.error) { - toastError(data.error); - return; - } + const menu = [ + { name: "Home", icon: , href: "/dashboard" }, + { name: "Profile", icon: , href: "/dashboard/profile" }, + { name: "Settings", icon: , href: "/dashboard/settings" }, + ]; - toastSuccess("Logged in!"); - router.push("/dashboard"); - } catch { - toastError("Something went wrong"); - } + const handleLogout = async () => { + await logout(); + router.push("/login"); }; return ( -
-
-
- -
-
- -
- Backlog.gg Logo +
+ {item.name} + + + ))} +
+ +
+
-
+ ); } diff --git a/frontend/app/dashboard/layout.tsx b/frontend/app/dashboard/layout.tsx new file mode 100644 index 0000000..164b7c2 --- /dev/null +++ b/frontend/app/dashboard/layout.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { getMe } from "@/lib/api"; +import FullscreenLoader from "@/components/dashboard/FullscreenLoader"; +import Sidebar from "@/components/dashboard/Sidebar"; + +export default function DashboardLayout({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchUser = async () => { + const data = await getMe(); + if (!data.user) router.push("/login"); + else setLoading(false); + }; + fetchUser(); + }, []); + + if (loading) return ; + + return ( +
+ +
{children}
+
+ ); +} diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index eae0d76..8f42ee4 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -1,34 +1,27 @@ "use client"; import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { getMe, logout, ApiResponse, User } from "@/lib/api"; -import { Button } from "@/components/ui/button"; +import { getMe, ApiResponse, User } from "@/lib/api"; export default function DashboardPage() { - const router = useRouter(); const [user, setUser] = useState(null); useEffect(() => { const fetchUser = async () => { const data: ApiResponse = await getMe(); - if (data.user) setUser(data.user); - else router.push("/login"); + if (data.user) { + setUser(data.user); + } }; - fetchUser(); - }, [router]); - - const handleLogout = async () => { - await logout(); - router.push("/login"); - }; - if (!user) return

Loading...

; + fetchUser(); + }, []); return (
-

Welcome, {user.username}!

- +

+ Welcome, {user?.username ?? "User"}! +

); -} \ No newline at end of file +} diff --git a/frontend/app/dashboard/profile/ProfileAvatar.tsx b/frontend/app/dashboard/profile/ProfileAvatar.tsx new file mode 100644 index 0000000..c546776 --- /dev/null +++ b/frontend/app/dashboard/profile/ProfileAvatar.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useState } from "react"; +import { toastSuccess, toastError } from "@/lib/toast"; + +export default function ProfileAvatar() { + const [preview, setPreview] = useState(null); + + const handleFile = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setPreview(URL.createObjectURL(file)); + + const formData = new FormData(); + formData.append("avatar", file); + + try { + const res = await fetch("/api/user/avatar", { + method: "POST", + body: formData, + }); + + if (!res.ok) throw new Error(); + + toastSuccess("Avatar updated!"); + } catch { + toastError("Failed to upload avatar"); + } + }; + + return ( +
+

Profile Picture

+ +
+ Avatar + + +
+
+ ); +} diff --git a/frontend/app/dashboard/profile/ProfileDangerZone.tsx b/frontend/app/dashboard/profile/ProfileDangerZone.tsx new file mode 100644 index 0000000..f73ced0 --- /dev/null +++ b/frontend/app/dashboard/profile/ProfileDangerZone.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Button } from "@/components/ui/button"; + +export default function ProfileDangerZone() { + const deleteAccount = async () => { + if (!confirm("Are you sure? This action is permanent.")) return; + + await fetch("/api/user/delete", { method: "DELETE" }); + window.location.href = "/"; + }; + + return ( +
+

Danger Zone

+ + + + +
+ ); +} diff --git a/frontend/app/dashboard/profile/ProfileDetails.tsx b/frontend/app/dashboard/profile/ProfileDetails.tsx new file mode 100644 index 0000000..faf1c79 --- /dev/null +++ b/frontend/app/dashboard/profile/ProfileDetails.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; + +export default function ProfileDetails() { + const [bio, setBio] = useState(""); + + const handleSave = async () => { + await fetch("/api/user/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ bio }), + }); + }; + + return ( +
+

Profile Information

+ +