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 */} +