From 144b13a0cf0f0111c07bafc2e6f2fa8a1089a9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?= Date: Thu, 13 Nov 2025 20:57:43 +0100 Subject: [PATCH] feat: add country and city fields to user profile - Add country and city fields to User model - Create database migration for location fields - Add validation for country and city (max 100 characters) - Create countries.js with complete list of 195 countries - Add country dropdown select and city text input to profile page - Include country and city in GET /api/users/me response - Update profile form to support location data Users can now select their country from a dropdown list of all countries and enter their city name. --- .../migration.sql | 3 + backend/prisma/schema.prisma | 4 + backend/src/controllers/user.js | 4 +- backend/src/middleware/validators.js | 10 + backend/src/routes/users.js | 2 + frontend/src/data/countries.js | 200 ++++++++++++++++++ frontend/src/pages/ProfilePage.jsx | 55 ++++- 7 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 backend/prisma/migrations/20251113194943_add_location_fields/migration.sql create mode 100644 frontend/src/data/countries.js diff --git a/backend/prisma/migrations/20251113194943_add_location_fields/migration.sql b/backend/prisma/migrations/20251113194943_add_location_fields/migration.sql new file mode 100644 index 0000000..292ceae --- /dev/null +++ b/backend/prisma/migrations/20251113194943_add_location_fields/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "city" VARCHAR(100), +ADD COLUMN "country" VARCHAR(100); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 23e6767..3b7584a 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -29,6 +29,10 @@ model User { facebookUrl String? @map("facebook_url") @db.VarChar(255) tiktokUrl String? @map("tiktok_url") @db.VarChar(255) + // Location + country String? @db.VarChar(100) + city String? @db.VarChar(100) + // 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 159d89b..56e4aa4 100644 --- a/backend/src/controllers/user.js +++ b/backend/src/controllers/user.js @@ -10,7 +10,7 @@ const { sanitizeForEmail } = require('../utils/sanitize'); async function updateProfile(req, res, next) { try { const userId = req.user.id; - const { firstName, lastName, email, wsdcId, youtubeUrl, instagramUrl, facebookUrl, tiktokUrl } = req.body; + const { firstName, lastName, email, wsdcId, youtubeUrl, instagramUrl, facebookUrl, tiktokUrl, country, city } = req.body; // Build update data const updateData = {}; @@ -21,6 +21,8 @@ async function updateProfile(req, res, next) { if (instagramUrl !== undefined) updateData.instagramUrl = instagramUrl || null; if (facebookUrl !== undefined) updateData.facebookUrl = facebookUrl || null; if (tiktokUrl !== undefined) updateData.tiktokUrl = tiktokUrl || null; + if (country !== undefined) updateData.country = country || null; + if (city !== undefined) updateData.city = city || 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 45351cd..c56f98c 100644 --- a/backend/src/middleware/validators.js +++ b/backend/src/middleware/validators.js @@ -168,6 +168,16 @@ const updateProfileValidation = [ return value.includes('tiktok.com'); }) .withMessage('Must be a valid TikTok URL (tiktok.com)'), + body('country') + .optional() + .trim() + .isLength({ max: 100 }) + .withMessage('Country must be less than 100 characters'), + body('city') + .optional() + .trim() + .isLength({ max: 100 }) + .withMessage('City must be less than 100 characters'), handleValidationErrors, ]; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index b06bb5d..2db9105 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -24,6 +24,8 @@ router.get('/me', authenticate, async (req, res, next) => { instagramUrl: true, facebookUrl: true, tiktokUrl: true, + country: true, + city: true, avatar: true, createdAt: true, updatedAt: true, diff --git a/frontend/src/data/countries.js b/frontend/src/data/countries.js new file mode 100644 index 0000000..df7c50f --- /dev/null +++ b/frontend/src/data/countries.js @@ -0,0 +1,200 @@ +// Complete list of countries (ISO 3166-1) +export const COUNTRIES = [ + 'Afghanistan', + 'Albania', + 'Algeria', + 'Andorra', + 'Angola', + 'Antigua and Barbuda', + 'Argentina', + 'Armenia', + 'Australia', + 'Austria', + 'Azerbaijan', + 'Bahamas', + 'Bahrain', + 'Bangladesh', + 'Barbados', + 'Belarus', + 'Belgium', + 'Belize', + 'Benin', + 'Bhutan', + 'Bolivia', + 'Bosnia and Herzegovina', + 'Botswana', + 'Brazil', + 'Brunei', + 'Bulgaria', + 'Burkina Faso', + 'Burundi', + 'Cabo Verde', + 'Cambodia', + 'Cameroon', + 'Canada', + 'Central African Republic', + 'Chad', + 'Chile', + 'China', + 'Colombia', + 'Comoros', + 'Congo', + 'Costa Rica', + 'Croatia', + 'Cuba', + 'Cyprus', + 'Czech Republic', + 'Denmark', + 'Djibouti', + 'Dominica', + 'Dominican Republic', + 'DR Congo', + 'Ecuador', + 'Egypt', + 'El Salvador', + 'Equatorial Guinea', + 'Eritrea', + 'Estonia', + 'Eswatini', + 'Ethiopia', + 'Fiji', + 'Finland', + 'France', + 'Gabon', + 'Gambia', + 'Georgia', + 'Germany', + 'Ghana', + 'Greece', + 'Grenada', + 'Guatemala', + 'Guinea', + 'Guinea-Bissau', + 'Guyana', + 'Haiti', + 'Honduras', + 'Hungary', + 'Iceland', + 'India', + 'Indonesia', + 'Iran', + 'Iraq', + 'Ireland', + 'Israel', + 'Italy', + 'Ivory Coast', + 'Jamaica', + 'Japan', + 'Jordan', + 'Kazakhstan', + 'Kenya', + 'Kiribati', + 'Kosovo', + 'Kuwait', + 'Kyrgyzstan', + 'Laos', + 'Latvia', + 'Lebanon', + 'Lesotho', + 'Liberia', + 'Libya', + 'Liechtenstein', + 'Lithuania', + 'Luxembourg', + 'Madagascar', + 'Malawi', + 'Malaysia', + 'Maldives', + 'Mali', + 'Malta', + 'Marshall Islands', + 'Mauritania', + 'Mauritius', + 'Mexico', + 'Micronesia', + 'Moldova', + 'Monaco', + 'Mongolia', + 'Montenegro', + 'Morocco', + 'Mozambique', + 'Myanmar', + 'Namibia', + 'Nauru', + 'Nepal', + 'Netherlands', + 'New Zealand', + 'Nicaragua', + 'Niger', + 'Nigeria', + 'North Korea', + 'North Macedonia', + 'Norway', + 'Oman', + 'Pakistan', + 'Palau', + 'Palestine', + 'Panama', + 'Papua New Guinea', + 'Paraguay', + 'Peru', + 'Philippines', + 'Poland', + 'Portugal', + 'Qatar', + 'Romania', + 'Russia', + 'Rwanda', + 'Saint Kitts and Nevis', + 'Saint Lucia', + 'Saint Vincent and the Grenadines', + 'Samoa', + 'San Marino', + 'Sao Tome and Principe', + 'Saudi Arabia', + 'Senegal', + 'Serbia', + 'Seychelles', + 'Sierra Leone', + 'Singapore', + 'Slovakia', + 'Slovenia', + 'Solomon Islands', + 'Somalia', + 'South Africa', + 'South Korea', + 'South Sudan', + 'Spain', + 'Sri Lanka', + 'Sudan', + 'Suriname', + 'Sweden', + 'Switzerland', + 'Syria', + 'Taiwan', + 'Tajikistan', + 'Tanzania', + 'Thailand', + 'Timor-Leste', + 'Togo', + 'Tonga', + 'Trinidad and Tobago', + 'Tunisia', + 'Turkey', + 'Turkmenistan', + 'Tuvalu', + 'Uganda', + 'Ukraine', + 'United Arab Emirates', + 'United Kingdom', + 'United States', + 'Uruguay', + 'Uzbekistan', + 'Vanuatu', + 'Vatican City', + 'Venezuela', + 'Vietnam', + 'Yemen', + 'Zambia', + 'Zimbabwe' +]; diff --git a/frontend/src/pages/ProfilePage.jsx b/frontend/src/pages/ProfilePage.jsx index 8d0f7c1..a276531 100644 --- a/frontend/src/pages/ProfilePage.jsx +++ b/frontend/src/pages/ProfilePage.jsx @@ -2,7 +2,8 @@ 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, Youtube, Instagram, Facebook } from 'lucide-react'; +import { User, Mail, Lock, Save, AlertCircle, CheckCircle, Loader2, Hash, Youtube, Instagram, Facebook, MapPin, Globe } from 'lucide-react'; +import { COUNTRIES } from '../data/countries'; const ProfilePage = () => { const { user, updateUser } = useAuth(); @@ -18,6 +19,8 @@ const ProfilePage = () => { instagramUrl: '', facebookUrl: '', tiktokUrl: '', + country: '', + city: '', }); // Load user data when component mounts or user changes @@ -32,6 +35,8 @@ const ProfilePage = () => { instagramUrl: user.instagramUrl || '', facebookUrl: user.facebookUrl || '', tiktokUrl: user.tiktokUrl || '', + country: user.country || '', + city: user.city || '', }); } }, [user]); @@ -231,6 +236,54 @@ const ProfilePage = () => { + {/* Location Section */} +
+ {/* Country */} +
+ +
+
+ +
+ +
+
+ + {/* City */} +
+ +
+
+ +
+ +
+
+
+ {/* WSDC ID */}