From 48f9dfe1b410a90a341941fd3e4c79ea9e9af151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?= Date: Thu, 13 Nov 2025 20:47:57 +0100 Subject: [PATCH] feat: add social media links to user profile - Add YouTube, Instagram, Facebook, and TikTok URL fields to User model - Create database migration for social media link columns - Add custom validators to ensure URLs contain correct domains - Update profile page with social media input fields - Include social media URLs in GET /api/users/me response - Add icons for each social platform in the UI Users can now add links to their social media profiles. Each field validates that the URL contains the appropriate domain (e.g., instagram.com for Instagram, youtube.com/youtu.be for YouTube). --- .../migration.sql | 5 + backend/prisma/schema.prisma | 6 ++ backend/src/controllers/user.js | 6 +- backend/src/middleware/validators.js | 32 ++++++ backend/src/routes/users.js | 4 + frontend/src/pages/ProfilePage.jsx | 99 ++++++++++++++++++- 6 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 backend/prisma/migrations/20251113194011_add_social_media_links/migration.sql diff --git a/backend/prisma/migrations/20251113194011_add_social_media_links/migration.sql b/backend/prisma/migrations/20251113194011_add_social_media_links/migration.sql new file mode 100644 index 0000000..28c895d --- /dev/null +++ b/backend/prisma/migrations/20251113194011_add_social_media_links/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "facebook_url" VARCHAR(255), +ADD COLUMN "instagram_url" VARCHAR(255), +ADD COLUMN "tiktok_url" VARCHAR(255), +ADD COLUMN "youtube_url" VARCHAR(255); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 01a3299..23e6767 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -23,6 +23,12 @@ model User { lastName String? @map("last_name") @db.VarChar(100) wsdcId String? @unique @map("wsdc_id") @db.VarChar(20) + // Social Media Links + youtubeUrl String? @map("youtube_url") @db.VarChar(255) + instagramUrl String? @map("instagram_url") @db.VarChar(255) + facebookUrl String? @map("facebook_url") @db.VarChar(255) + tiktokUrl String? @map("tiktok_url") @db.VarChar(255) + // Email Verification (Phase 1.5) emailVerified Boolean @default(false) @map("email_verified") verificationToken String? @unique @map("verification_token") @db.VarChar(255) diff --git a/backend/src/controllers/user.js b/backend/src/controllers/user.js index 33074f1..159d89b 100644 --- a/backend/src/controllers/user.js +++ b/backend/src/controllers/user.js @@ -10,13 +10,17 @@ const { sanitizeForEmail } = require('../utils/sanitize'); async function updateProfile(req, res, next) { try { const userId = req.user.id; - const { firstName, lastName, email, wsdcId } = req.body; + const { firstName, lastName, email, wsdcId, youtubeUrl, instagramUrl, facebookUrl, tiktokUrl } = req.body; // Build update data const updateData = {}; if (firstName !== undefined) updateData.firstName = firstName; if (lastName !== undefined) updateData.lastName = lastName; if (wsdcId !== undefined) updateData.wsdcId = wsdcId || null; // Allow empty string to clear WSDC ID + if (youtubeUrl !== undefined) updateData.youtubeUrl = youtubeUrl || null; + if (instagramUrl !== undefined) updateData.instagramUrl = instagramUrl || null; + if (facebookUrl !== undefined) updateData.facebookUrl = facebookUrl || null; + if (tiktokUrl !== undefined) updateData.tiktokUrl = tiktokUrl || null; // Check if email is being changed const currentUser = await prisma.user.findUnique({ diff --git a/backend/src/middleware/validators.js b/backend/src/middleware/validators.js index 481002e..45351cd 100644 --- a/backend/src/middleware/validators.js +++ b/backend/src/middleware/validators.js @@ -136,6 +136,38 @@ const updateProfileValidation = [ .trim() .matches(/^\d{0,10}$/) .withMessage('WSDC ID must be numeric and up to 10 digits'), + body('youtubeUrl') + .optional() + .trim() + .custom((value) => { + if (!value) return true; // Allow empty + return value.includes('youtube.com') || value.includes('youtu.be'); + }) + .withMessage('Must be a valid YouTube URL (youtube.com or youtu.be)'), + body('instagramUrl') + .optional() + .trim() + .custom((value) => { + if (!value) return true; // Allow empty + return value.includes('instagram.com'); + }) + .withMessage('Must be a valid Instagram URL (instagram.com)'), + body('facebookUrl') + .optional() + .trim() + .custom((value) => { + if (!value) return true; // Allow empty + return value.includes('facebook.com') || value.includes('fb.com'); + }) + .withMessage('Must be a valid Facebook URL (facebook.com or fb.com)'), + body('tiktokUrl') + .optional() + .trim() + .custom((value) => { + if (!value) return true; // Allow empty + return value.includes('tiktok.com'); + }) + .withMessage('Must be a valid TikTok URL (tiktok.com)'), handleValidationErrors, ]; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 6466eb2..b06bb5d 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -20,6 +20,10 @@ router.get('/me', authenticate, async (req, res, next) => { firstName: true, lastName: true, wsdcId: true, + youtubeUrl: true, + instagramUrl: true, + facebookUrl: true, + tiktokUrl: true, avatar: true, createdAt: true, updatedAt: true, diff --git a/frontend/src/pages/ProfilePage.jsx b/frontend/src/pages/ProfilePage.jsx index 9ba5fec..8d0f7c1 100644 --- a/frontend/src/pages/ProfilePage.jsx +++ b/frontend/src/pages/ProfilePage.jsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { authAPI } from '../services/api'; import Layout from '../components/layout/Layout'; -import { User, Mail, Lock, Save, AlertCircle, CheckCircle, Loader2, Hash } from 'lucide-react'; +import { User, Mail, Lock, Save, AlertCircle, CheckCircle, Loader2, Hash, Youtube, Instagram, Facebook } from 'lucide-react'; const ProfilePage = () => { const { user, updateUser } = useAuth(); @@ -14,6 +14,10 @@ const ProfilePage = () => { lastName: '', email: '', wsdcId: '', + youtubeUrl: '', + instagramUrl: '', + facebookUrl: '', + tiktokUrl: '', }); // Load user data when component mounts or user changes @@ -24,6 +28,10 @@ const ProfilePage = () => { lastName: user.lastName || '', email: user.email || '', wsdcId: user.wsdcId || '', + youtubeUrl: user.youtubeUrl || '', + instagramUrl: user.instagramUrl || '', + facebookUrl: user.facebookUrl || '', + tiktokUrl: user.tiktokUrl || '', }); } }, [user]); @@ -272,6 +280,95 @@ const ProfilePage = () => {

+ {/* Social Media Links Section */} +
+

Social Media Links

+ +
+ {/* YouTube */} +
+ +
+
+ +
+ +
+
+ + {/* Instagram */} +
+ +
+
+ +
+ +
+
+ + {/* Facebook */} +
+ +
+
+ +
+ +
+
+ + {/* TikTok */} +
+ +
+
+ + + +
+ +
+
+
+
+ {/* Submit Button */}