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.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "city" VARCHAR(100),
|
||||
ADD COLUMN "country" VARCHAR(100);
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
200
frontend/src/data/countries.js
Normal file
200
frontend/src/data/countries.js
Normal file
@@ -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'
|
||||
];
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location Section */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Country */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Country
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Globe className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<select
|
||||
name="country"
|
||||
value={profileData.country}
|
||||
onChange={handleProfileChange}
|
||||
className="pl-10 w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value="">Select a country</option>
|
||||
{COUNTRIES.map((country) => (
|
||||
<option key={country} value={country}>
|
||||
{country}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* City */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
City
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<MapPin className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="city"
|
||||
value={profileData.city}
|
||||
onChange={handleProfileChange}
|
||||
className="pl-10 w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Somewhere"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* WSDC ID */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
|
||||
Reference in New Issue
Block a user