Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/src/middleware/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ export const validateProfileCreation: ValidationChain[] = [
.isLength({ max: 100 })
.customSanitizer(sanitizeText),

body('minor')
.optional()
.isArray({ max: 4 })
.withMessage('Maximum 4 minors allowed'),

body('minor.*')
.optional()
.trim()
.isLength({ max: 100 })
.customSanitizer(sanitizeText),

body('interests')
.optional()
.isArray({ max: 10 })
Expand Down
1 change: 1 addition & 0 deletions backend/src/routes/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const profileDocToResponse = (
year: doc.year,
school: doc.school,
major: doc.major,
minor: doc.minor,
pictures: doc.pictures,
createdAt:
doc.createdAt instanceof Date
Expand Down
2 changes: 2 additions & 0 deletions backend/src/utils/profilePrivacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function filterProfileByPrivacy(
year: profile.year,
school: profile.school,
major: profile.major,
minor: profile.minor,
pictures: profile.pictures,
prompts: profile.prompts,
interests: profile.interests,
Expand Down Expand Up @@ -79,6 +80,7 @@ export function filterProfileByPrivacy(
interests: profile.interests,
clubs: profile.clubs,
major: profile.major,
minor: profile.minor,
// Only show if privacy setting allows
gender: profile.showGenderOnProfile ? profile.gender : undefined,
pronouns: profile.showPronounsOnProfile ? profile.pronouns : undefined,
Expand Down
11 changes: 7 additions & 4 deletions backend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface ProfileDocWrite {
year: Year;
school: School;
major: string[];
minor?: string[];
pictures: string[];
createdAt: FirestoreTimestampType | FieldValue;
updatedAt: FirestoreTimestampType | FieldValue;
Expand Down Expand Up @@ -128,6 +129,7 @@ export interface ProfileDoc {
year: Year;
school: School;
major: string[];
minor?: string[];
pictures: string[]; // URLs to images in Firebase Storage
createdAt: FirestoreTimestampType;
updatedAt: FirestoreTimestampType;
Expand Down Expand Up @@ -173,6 +175,7 @@ export interface ProfileResponse {
year: Year;
school: School;
major: string[];
minor?: string[];
pictures: string[];
createdAt: string; // ISO string format for JSON
updatedAt: string; // ISO string format for JSON
Expand Down Expand Up @@ -200,10 +203,10 @@ export type UpdateProfileInput = Partial<
// Convert Firestore document to API response
export type DocToResponse<T extends Record<string, any>> = {
[K in keyof T]: T[K] extends FirestoreTimestampType
? string
: T[K] extends FirestoreTimestampType | undefined
? string | undefined
: T[K];
? string
: T[K] extends FirestoreTimestampType | undefined
? string | undefined
: T[K];
};

// Helper type for Firestore document with auto-generated ID
Expand Down
111 changes: 108 additions & 3 deletions frontend/app/(auth)/edit-education.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Alert, ScrollView, StatusBar, StyleSheet, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
CORNELL_MAJORS,
CORNELL_MINORS,
CORNELL_SCHOOLS,
Year,
YEARS,
Expand Down Expand Up @@ -37,11 +38,13 @@ export default function EditEducationPage() {
// Current values
const [school, setSchool] = useState<School | ''>('');
const [majors, setMajors] = useState<string[]>([]);
const [minors, setMinors] = useState<string[]>([]);
const [year, setYear] = useState<Year | null>(null);

// Original values for comparison
const [originalSchool, setOriginalSchool] = useState<School | ''>('');
const [originalMajors, setOriginalMajors] = useState<string[]>([]);
const [originalMinors, setOriginalMinors] = useState<string[]>([]);
const [originalYear, setOriginalYear] = useState<Year | null>(null);

// Visibility preference
Expand All @@ -51,18 +54,22 @@ export default function EditEducationPage() {
// Sheet states
const [showSchoolSheet, setShowSchoolSheet] = useState(false);
const [showMajorSheet, setShowMajorSheet] = useState(false);
const [showMinorSheet, setShowMinorSheet] = useState(false);
const [showYearSheet, setShowYearSheet] = useState(false);
const [majorSearchQuery, setMajorSearchQuery] = useState('');
const [minorSearchQuery, setMinorSearchQuery] = useState('');
const [showUnsavedChangesSheet, setShowUnsavedChangesSheet] = useState(false);

useEffect(() => {
if (profile) {
setSchool(profile.school);
setMajors(profile.major);
setMinors(profile.minor ?? []);
setYear(profile.year);

setOriginalSchool(profile.school);
setOriginalMajors(profile.major);
setOriginalMinors(profile.minor ?? []);
setOriginalYear(profile.year);

const showCollegeValue = profile.showCollegeOnProfile ?? true;
Expand Down Expand Up @@ -96,6 +103,7 @@ export default function EditEducationPage() {
await updateProfile({
school,
major: majors,
minor: minors,
year,
showCollegeOnProfile: showOnProfile,
});
Expand All @@ -104,12 +112,14 @@ export default function EditEducationPage() {
updateProfileData({
school,
major: majors,
minor: minors,
year,
showCollegeOnProfile: showOnProfile,
});

setOriginalSchool(school);
setOriginalMajors(majors);
setOriginalMinors(minors);
setOriginalYear(year);
setOriginalShowOnProfile(showOnProfile);

Expand All @@ -135,7 +145,8 @@ export default function EditEducationPage() {
school !== originalSchool ||
year !== originalYear ||
showOnProfile !== originalShowOnProfile ||
JSON.stringify(majors.sort()) !== JSON.stringify(originalMajors.sort())
JSON.stringify(majors.sort()) !== JSON.stringify(originalMajors.sort()) ||
JSON.stringify(minors.sort()) !== JSON.stringify(originalMinors.sort())
);
};

Expand All @@ -160,6 +171,10 @@ export default function EditEducationPage() {
setMajors((prev) => prev.filter((_, i) => i !== index));
};

const removeMinor = (index: number) => {
setMinors((prev) => prev.filter((_, i) => i !== index));
};

return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
Expand Down Expand Up @@ -212,7 +227,35 @@ export default function EditEducationPage() {
onPress={() => setShowMajorSheet(true)}
variant="secondary"
noRound
disabled={!school}
disabled={!school || majors.length >= 3}
/>
</ListItemWrapper>
</View>

{/* Minors */}
<View style={styles.section}>
<AppText indented>Minor(s)</AppText>
<ListItemWrapper>
{minors.length > 0 && (
<View style={styles.tagsContainer}>
{minors.map((minor, index) => (
<Tag
key={minor}
variant="white"
label={minor}
dismissible
onDismiss={() => removeMinor(index)}
/>
))}
</View>
)}
<Button
title="Add minor"
iconLeft={Plus}
onPress={() => setShowMinorSheet(true)}
variant="secondary"
noRound
disabled={!school || minors.length >= 4}
/>
</ListItemWrapper>
</View>
Expand Down Expand Up @@ -299,7 +342,7 @@ export default function EditEducationPage() {
{school &&
(CORNELL_MAJORS[school] || [])
.filter((major) =>
major.toLowerCase().includes(majorSearchQuery.toLowerCase())
major.toLowerCase().includes(majorSearchQuery.toLowerCase()) && !majors.includes(major)
)
.map((major) => (
<ListItem
Expand Down Expand Up @@ -339,6 +382,68 @@ export default function EditEducationPage() {
</View>
</Sheet>

{/* Minor Selection Sheet */}
<Sheet
visible={showMinorSheet && !!school}
onDismiss={() => {
setShowMinorSheet(false);
setMinorSearchQuery('');
}}
title="Select your minor"
bottomRound={false}
>
<View style={styles.majorSheetContent}>
<AppInput
placeholder="Search for your minor"
value={minorSearchQuery}
onChangeText={setMinorSearchQuery}
autoFocus
/>
<ScrollView style={styles.majorScrollView}>
<ListItemWrapper>
{school &&
(CORNELL_MINORS || [])
.filter((m) =>
m.toLowerCase().includes(minorSearchQuery.toLowerCase()) && !minors.includes(m)
)
.map((m) => (
<ListItem
key={m}
title={m}
onPress={() => {
if (!minors.includes(m)) {
setMinors([...minors, m]);
}
setShowMinorSheet(false);
setMinorSearchQuery('');
}}
/>
))}
{minorSearchQuery.trim() &&
school &&
(CORNELL_MINORS || []).filter((m) =>
m.toLowerCase().includes(minorSearchQuery.toLowerCase())
).length === 0 && (
<View style={styles.emptyState}>
<AppText style={styles.emptyText}>No results found</AppText>
<Button
title={`Use "${minorSearchQuery}"`}
onPress={() => {
if (!minors.includes(minorSearchQuery.trim())) {
setMinors([...minors, minorSearchQuery.trim()]);
}
setShowMinorSheet(false);
setMinorSearchQuery('');
}}
variant="primary"
/>
</View>
)}
</ListItemWrapper>
</ScrollView>
</View>
</Sheet>

{/* Year Selection Sheet */}
<Sheet
visible={showYearSheet}
Expand Down
19 changes: 12 additions & 7 deletions frontend/app/(auth)/edit-profile.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import AppText from '@/app/components/ui/AppText';
import { ProfileResponse, PromptData, getProfileAge } from '@/types';
import { router, useFocusEffect } from 'expo-router';
import { PromptData, getProfileAge } from '@/types';
import { router } from 'expo-router';
import {
Check,
ChevronRight,
Expand All @@ -9,7 +9,7 @@
Pencil,
Plus,
} from 'lucide-react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
Alert,
Linking,
Expand Down Expand Up @@ -97,7 +97,7 @@
useThemeAware(); // Force re-render when theme changes
const { showToast } = useToast();
const haptic = useHapticFeedback();
const { profile: profileData, loading, refreshProfile, updateProfileData } = useProfile();

Check warning on line 100 in frontend/app/(auth)/edit-profile.tsx

View workflow job for this annotation

GitHub Actions / Frontend - Build & Type Check

'loading' is assigned a value but never used
const [prompts, setPrompts] = useState<PromptData[]>([]);
const [showUnsavedSheet, setShowUnsavedSheet] = useState(false);
const [originalPrompts, setOriginalPrompts] = useState<PromptData[]>([]);
Expand Down Expand Up @@ -148,7 +148,7 @@
}
}, [profileData]);

const openURL = async (url: string) => {

Check warning on line 151 in frontend/app/(auth)/edit-profile.tsx

View workflow job for this annotation

GitHub Actions / Frontend - Build & Type Check

'openURL' is assigned a value but never used
try {
const supported = await Linking.canOpenURL(url);
if (supported) {
Expand Down Expand Up @@ -313,7 +313,7 @@
}
};

const addPrompt = () => {

Check warning on line 316 in frontend/app/(auth)/edit-profile.tsx

View workflow job for this annotation

GitHub Actions / Frontend - Build & Type Check

'addPrompt' is assigned a value but never used
if (prompts.length < 3) {
const newPrompt: PromptData = {
id: Date.now().toString(),
Expand All @@ -324,11 +324,11 @@
}
};

const updatePrompt = (id: string, updatedPrompt: PromptData) => {

Check warning on line 327 in frontend/app/(auth)/edit-profile.tsx

View workflow job for this annotation

GitHub Actions / Frontend - Build & Type Check

'updatePrompt' is assigned a value but never used
setPrompts(prompts.map((p) => (p.id === id ? updatedPrompt : p)));
};

const removePrompt = (id: string) => {

Check warning on line 331 in frontend/app/(auth)/edit-profile.tsx

View workflow job for this annotation

GitHub Actions / Frontend - Build & Type Check

'removePrompt' is assigned a value but never used
setPrompts(prompts.filter((p) => p.id !== id));
};

Expand All @@ -350,6 +350,10 @@
profileData?.major && profileData.major.length > 0
? profileData.major.join(', ')
: 'Major not set';
const displayMinor =
profileData?.minor && profileData.minor.length > 0
? profileData.minor.join(', ')
: null;
const displayYear = profileData?.year || 'Year not set';
// Social fields are only available on OwnProfileResponse
const displayInstagram =
Expand Down Expand Up @@ -563,7 +567,7 @@
description={
profileData?.gender
? profileData.gender.charAt(0).toUpperCase() +
profileData.gender.slice(1)
profileData.gender.slice(1)
: ''
}
right={<ChevronRight size={20} />}
Expand All @@ -588,11 +592,12 @@
title="Education"
description={
<AppText color="dimmer">
<AppText>{profileData?.year}</AppText>
{profileData?.year}
{' in '}
<AppText>{profileData?.school}</AppText>
{profileData?.school}
{' studying '}
<AppText>{profileData?.major?.join(', ')}</AppText>
{profileData?.major?.join(', ')}
{displayMinor ? ` minoring in ${displayMinor}` : ''}
</AppText>
}
right={<ChevronRight size={20} />}
Expand Down
Loading
Loading