diff --git a/functions/src/rankings.js b/functions/src/rankings.js index 3005634..b44d605 100644 --- a/functions/src/rankings.js +++ b/functions/src/rankings.js @@ -1,8 +1,7 @@ const admin = require("firebase-admin"); const { onSchedule } = require("firebase-functions/v2/scheduler"); -const { logger } = require("firebase-functions/logger"); - -const db = admin.firestore(); +const { refList } = require("../index"); +const firestore = refList.projects.firestore; function buildMonthContext(referenceDate) { const current = referenceDate ? new Date(referenceDate) : new Date(); @@ -35,15 +34,10 @@ exports.snapshotMonthlyTopSongs = onSchedule( async (event) => { const { scheduleTime } = event; const context = buildMonthContext( - scheduleTime ? new Date(scheduleTime) : new Date(), + scheduleTime ? new Date(scheduleTime) : new Date() ); - logger.info( - `[snapshotMonthlyTopSongs] Start ranking for ${context.monthKey}`, - ); - - const topProjectsSnap = await db - .collection("projects") + const topProjectsSnap = await refList.projects .orderBy("views", "desc") .limit(3) .get(); @@ -62,7 +56,9 @@ exports.snapshotMonthlyTopSongs = onSchedule( }; }); - const docRef = db.collection("monthlyTopSongs").doc(context.monthKey); + const docRef = firestore + .collection("monthlyTopSongs") + .doc(context.monthKey); const existingSnapshot = await docRef.get(); const payload = { @@ -83,9 +79,5 @@ exports.snapshotMonthlyTopSongs = onSchedule( } await docRef.set(payload, { merge: true }); - - logger.info( - `[snapshotMonthlyTopSongs] Stored ${topProjects.length} projects for ${context.monthKey}`, - ); - }, + } ); diff --git a/src/constants/countries.js b/src/constants/countries.js new file mode 100644 index 0000000..d518e67 --- /dev/null +++ b/src/constants/countries.js @@ -0,0 +1,199 @@ +const COUNTRIES = [ + { code: "AF", name: "Afghanistan" }, + { code: "AL", name: "Albania" }, + { code: "DZ", name: "Algeria" }, + { code: "AD", name: "Andorra" }, + { code: "AO", name: "Angola" }, + { code: "AG", name: "Antigua and Barbuda" }, + { code: "AR", name: "Argentina" }, + { code: "AM", name: "Armenia" }, + { code: "AU", name: "Australia" }, + { code: "AT", name: "Austria" }, + { code: "AZ", name: "Azerbaijan" }, + { code: "BS", name: "Bahamas" }, + { code: "BH", name: "Bahrain" }, + { code: "BD", name: "Bangladesh" }, + { code: "BB", name: "Barbados" }, + { code: "BY", name: "Belarus" }, + { code: "BE", name: "Belgium" }, + { code: "BZ", name: "Belize" }, + { code: "BJ", name: "Benin" }, + { code: "BT", name: "Bhutan" }, + { code: "BO", name: "Bolivia" }, + { code: "BA", name: "Bosnia and Herzegovina" }, + { code: "BW", name: "Botswana" }, + { code: "BR", name: "Brazil" }, + { code: "BN", name: "Brunei" }, + { code: "BG", name: "Bulgaria" }, + { code: "BF", name: "Burkina Faso" }, + { code: "BI", name: "Burundi" }, + { code: "CV", name: "Cape Verde" }, + { code: "KH", name: "Cambodia" }, + { code: "CM", name: "Cameroon" }, + { code: "CA", name: "Canada" }, + { code: "CF", name: "Central African Republic" }, + { code: "TD", name: "Chad" }, + { code: "CL", name: "Chile" }, + { code: "CN", name: "China" }, + { code: "CO", name: "Colombia" }, + { code: "KM", name: "Comoros" }, + { code: "CG", name: "Congo" }, + { code: "CD", name: "Congo (Democratic Republic)" }, + { code: "CR", name: "Costa Rica" }, + { code: "CI", name: "Cote d'Ivoire" }, + { code: "HR", name: "Croatia" }, + { code: "CU", name: "Cuba" }, + { code: "CY", name: "Cyprus" }, + { code: "CZ", name: "Czech Republic" }, + { code: "DK", name: "Denmark" }, + { code: "DJ", name: "Djibouti" }, + { code: "DM", name: "Dominica" }, + { code: "DO", name: "Dominican Republic" }, + { code: "EC", name: "Ecuador" }, + { code: "EG", name: "Egypt" }, + { code: "SV", name: "El Salvador" }, + { code: "GQ", name: "Equatorial Guinea" }, + { code: "ER", name: "Eritrea" }, + { code: "EE", name: "Estonia" }, + { code: "SZ", name: "Eswatini" }, + { code: "ET", name: "Ethiopia" }, + { code: "FJ", name: "Fiji" }, + { code: "FI", name: "Finland" }, + { code: "FR", name: "France" }, + { code: "GA", name: "Gabon" }, + { code: "GM", name: "Gambia" }, + { code: "GE", name: "Georgia" }, + { code: "DE", name: "Germany" }, + { code: "GH", name: "Ghana" }, + { code: "GR", name: "Greece" }, + { code: "GD", name: "Grenada" }, + { code: "GT", name: "Guatemala" }, + { code: "GN", name: "Guinea" }, + { code: "GW", name: "Guinea-Bissau" }, + { code: "GY", name: "Guyana" }, + { code: "HT", name: "Haiti" }, + { code: "HN", name: "Honduras" }, + { code: "HU", name: "Hungary" }, + { code: "IS", name: "Iceland" }, + { code: "IN", name: "India" }, + { code: "ID", name: "Indonesia" }, + { code: "IR", name: "Iran" }, + { code: "IQ", name: "Iraq" }, + { code: "IE", name: "Ireland" }, + { code: "IL", name: "Israel" }, + { code: "IT", name: "Italy" }, + { code: "JM", name: "Jamaica" }, + { code: "JP", name: "Japan" }, + { code: "JO", name: "Jordan" }, + { code: "KZ", name: "Kazakhstan" }, + { code: "KE", name: "Kenya" }, + { code: "KI", name: "Kiribati" }, + { code: "KW", name: "Kuwait" }, + { code: "KG", name: "Kyrgyzstan" }, + { code: "LA", name: "Laos" }, + { code: "LV", name: "Latvia" }, + { code: "LB", name: "Lebanon" }, + { code: "LS", name: "Lesotho" }, + { code: "LR", name: "Liberia" }, + { code: "LY", name: "Libya" }, + { code: "LI", name: "Liechtenstein" }, + { code: "LT", name: "Lithuania" }, + { code: "LU", name: "Luxembourg" }, + { code: "MG", name: "Madagascar" }, + { code: "MW", name: "Malawi" }, + { code: "MY", name: "Malaysia" }, + { code: "MV", name: "Maldives" }, + { code: "ML", name: "Mali" }, + { code: "MT", name: "Malta" }, + { code: "MH", name: "Marshall Islands" }, + { code: "MR", name: "Mauritania" }, + { code: "MU", name: "Mauritius" }, + { code: "MX", name: "Mexico" }, + { code: "FM", name: "Micronesia" }, + { code: "MD", name: "Moldova" }, + { code: "MC", name: "Monaco" }, + { code: "MN", name: "Mongolia" }, + { code: "ME", name: "Montenegro" }, + { code: "MA", name: "Morocco" }, + { code: "MZ", name: "Mozambique" }, + { code: "MM", name: "Myanmar" }, + { code: "NA", name: "Namibia" }, + { code: "NR", name: "Nauru" }, + { code: "NP", name: "Nepal" }, + { code: "NL", name: "Netherlands" }, + { code: "NZ", name: "New Zealand" }, + { code: "NI", name: "Nicaragua" }, + { code: "NE", name: "Niger" }, + { code: "NG", name: "Nigeria" }, + { code: "KP", name: "North Korea" }, + { code: "MK", name: "North Macedonia" }, + { code: "NO", name: "Norway" }, + { code: "OM", name: "Oman" }, + { code: "PK", name: "Pakistan" }, + { code: "PW", name: "Palau" }, + { code: "PS", name: "Palestine" }, + { code: "PA", name: "Panama" }, + { code: "PG", name: "Papua New Guinea" }, + { code: "PY", name: "Paraguay" }, + { code: "PE", name: "Peru" }, + { code: "PH", name: "Philippines" }, + { code: "PL", name: "Poland" }, + { code: "PT", name: "Portugal" }, + { code: "QA", name: "Qatar" }, + { code: "RO", name: "Romania" }, + { code: "RU", name: "Russia" }, + { code: "RW", name: "Rwanda" }, + { code: "KN", name: "Saint Kitts and Nevis" }, + { code: "LC", name: "Saint Lucia" }, + { code: "VC", name: "Saint Vincent and the Grenadines" }, + { code: "WS", name: "Samoa" }, + { code: "SM", name: "San Marino" }, + { code: "ST", name: "Sao Tome and Principe" }, + { code: "SA", name: "Saudi Arabia" }, + { code: "SN", name: "Senegal" }, + { code: "RS", name: "Serbia" }, + { code: "SC", name: "Seychelles" }, + { code: "SL", name: "Sierra Leone" }, + { code: "SG", name: "Singapore" }, + { code: "SK", name: "Slovakia" }, + { code: "SI", name: "Slovenia" }, + { code: "SB", name: "Solomon Islands" }, + { code: "SO", name: "Somalia" }, + { code: "ZA", name: "South Africa" }, + { code: "KR", name: "South Korea" }, + { code: "SS", name: "South Sudan" }, + { code: "ES", name: "Spain" }, + { code: "LK", name: "Sri Lanka" }, + { code: "SD", name: "Sudan" }, + { code: "SR", name: "Suriname" }, + { code: "SE", name: "Sweden" }, + { code: "CH", name: "Switzerland" }, + { code: "SY", name: "Syria" }, + { code: "TJ", name: "Tajikistan" }, + { code: "TZ", name: "Tanzania" }, + { code: "TH", name: "Thailand" }, + { code: "TL", name: "Timor-Leste" }, + { code: "TG", name: "Togo" }, + { code: "TO", name: "Tonga" }, + { code: "TT", name: "Trinidad and Tobago" }, + { code: "TN", name: "Tunisia" }, + { code: "TR", name: "Turkey" }, + { code: "TM", name: "Turkmenistan" }, + { code: "TV", name: "Tuvalu" }, + { code: "UG", name: "Uganda" }, + { code: "UA", name: "Ukraine" }, + { code: "AE", name: "United Arab Emirates" }, + { code: "GB", name: "United Kingdom" }, + { code: "US", name: "United States" }, + { code: "UY", name: "Uruguay" }, + { code: "UZ", name: "Uzbekistan" }, + { code: "VU", name: "Vanuatu" }, + { code: "VA", name: "Vatican City" }, + { code: "VE", name: "Venezuela" }, + { code: "VN", name: "Vietnam" }, + { code: "YE", name: "Yemen" }, + { code: "ZM", name: "Zambia" }, + { code: "ZW", name: "Zimbabwe" }, +]; + +export default COUNTRIES; diff --git a/src/screens/CreatePassword.js b/src/screens/CreatePassword.js index 0c1ade5..ae583e1 100644 --- a/src/screens/CreatePassword.js +++ b/src/screens/CreatePassword.js @@ -18,6 +18,10 @@ import { FONT_FAMILY } from "../styles/Fonts"; const CreatePassword = () => { const route = useRoute(); const email = route?.params?.email || ""; + const firstName = route?.params?.firstName || ""; + const lastName = route?.params?.lastName || ""; + const country = route?.params?.country || null; + const preferredLanguage = route?.params?.preferredLanguage || null; const [password, setPassword] = useState(""); const [passwordError, setPasswordError] = useState(""); const [loading, setLoading] = useState(false); @@ -62,9 +66,18 @@ const CreatePassword = () => { .createUserWithEmailAndPassword(email.trim(), password); const uid = cred?.user?.uid || firebase.auth().currentUser?.uid; if (!uid) throw new Error("Création de compte échouée"); + const trimmedFirstName = + typeof firstName === "string" ? firstName.trim() : ""; + const trimmedLastName = + typeof lastName === "string" ? lastName.trim() : ""; await usersRef.doc(uid).set( { email: email.trim(), + ...(trimmedFirstName ? { firstName: trimmedFirstName } : {}), + ...(trimmedLastName ? { lastName: trimmedLastName } : {}), + ...(preferredLanguage ? { preferredLanguage } : {}), + ...(country?.code ? { countryCode: country.code } : {}), + ...(country?.name ? { countryName: country.name } : {}), createdAt: firebase.firestore.FieldValue.serverTimestamp(), updatedAt: firebase.firestore.FieldValue.serverTimestamp(), }, diff --git a/src/screens/LandingPage.js b/src/screens/LandingPage.js index bbe71de..79ff7d7 100644 --- a/src/screens/LandingPage.js +++ b/src/screens/LandingPage.js @@ -1,6 +1,7 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; import { useNavigation } from "@react-navigation/native"; -import { useCallback, useState } from "react"; -import { View } from "react-native"; +import { useCallback, useEffect, useState } from "react"; +import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; import { background } from "../assets"; import FullscreenIntroVideo from "../components/FullscreenIntroVideo"; import GradientButton from "../components/GradientButton"; @@ -10,16 +11,36 @@ import { isWeb } from "../hooks/useLayoutType"; import Page from "../layouts/Page"; import { Routes } from "../navigation/Routes"; import { useUser } from "../providers/UserDataProvider"; +import { Palette } from "../styles"; +import { FONT_FAMILY } from "../styles/Fonts"; + +const LANGUAGE_STORAGE_KEY = "preferredLanguage"; export default function LandingPage() { const navigation = useNavigation(); const { currentUID } = useUser(); const [isVideoVisible, setIsVideoVisible] = useState(false); + const [selectedLanguage, setSelectedLanguage] = useState(null); const handleStartVisit = useCallback(() => { setIsVideoVisible(true); }, []); + const handleSelectLanguage = useCallback((language) => { + setSelectedLanguage(language); + AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, language).catch((error) => { + console.warn("LandingPage: failed to persist language", error); + }); + }, []); + + const handleSashaHome = useCallback(() => { + // Placeholder handler until the Sasha experience is available. + }, []); + + const handleLaunchAdventure = useCallback(() => { + navigation.navigate(Routes.Register); + }, [navigation]); + const persistAdventureStarted = useCallback(async () => { if (!currentUID) { return; @@ -44,11 +65,28 @@ export default function LandingPage() { simpleRef: true, }); + useEffect(() => { + let isMounted = true; + AsyncStorage.getItem(LANGUAGE_STORAGE_KEY) + .then((storedLanguage) => { + if (storedLanguage && isMounted) { + setSelectedLanguage(storedLanguage); + } + }) + .catch((error) => { + console.warn("LandingPage: failed to load language", error); + }); + + return () => { + isMounted = false; + }; + }, []); + return ( <> @@ -59,10 +97,43 @@ export default function LandingPage() { alignItems: "center", }} > - + + {!selectedLanguage && ( + + handleSelectLanguage("fr")} + > + 🇫🇷 Français + + handleSelectLanguage("en")} + > + 🇬🇧 English + + + )} + {selectedLanguage && ( + + + + + + )} + ); } + +const styles = StyleSheet.create({ + content: { + width: "100%", + maxWidth: 360, + alignItems: "center", + gap: 32, + }, + languageRow: { + flexDirection: "row", + gap: 16, + justifyContent: "center", + }, + languageButton: { + borderRadius: 14, + borderWidth: 1, + borderColor: Palette.ultraLightWhite, + backgroundColor: Palette.ultraLightWhite, + paddingVertical: 12, + paddingHorizontal: 18, + minWidth: 140, + alignItems: "center", + }, + languageText: { + fontSize: 16, + color: Palette.white, + fontFamily: FONT_FAMILY.InterSemiBold, + }, + actions: { + width: "100%", + maxWidth: 320, + gap: 16, + }, + actionButton: { + width: "100%", + }, +}); diff --git a/src/screens/Register.js b/src/screens/Register.js index 14917f2..51481df 100644 --- a/src/screens/Register.js +++ b/src/screens/Register.js @@ -1,9 +1,19 @@ -import React, { useState } from "react"; -import { Pressable, Text, View } from "react-native"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import React, { useEffect, useMemo, useState } from "react"; +import { + FlatList, + Modal, + Pressable, + StyleSheet, + Text, + TextInput, + View, +} from "react-native"; import { background } from "../assets"; import BorderGradientButton from "../components/BorderGradientButton"; import { Input } from "../components/Input"; import ItemContainer from "../components/ItemContainer/ItemContainer"; +import COUNTRIES from "../constants/countries"; import { isWeb } from "../hooks/useLayoutType"; import Page from "../layouts/Page"; import { Routes } from "../navigation"; @@ -11,8 +21,55 @@ import { navigate } from "../navigation/NavigationService"; import { Palette } from "../styles"; import { FONT_FAMILY } from "../styles/Fonts"; +const LANGUAGE_STORAGE_KEY = "preferredLanguage"; + const Register = () => { const [email, setEmail] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [selectedCountry, setSelectedCountry] = useState(null); + const [countrySearch, setCountrySearch] = useState(""); + const [isCountryModalVisible, setIsCountryModalVisible] = useState(false); + const [preferredLanguage, setPreferredLanguage] = useState(null); + + useEffect(() => { + AsyncStorage.getItem(LANGUAGE_STORAGE_KEY) + .then((storedLanguage) => { + if (storedLanguage) { + setPreferredLanguage(storedLanguage); + } + }) + .catch((error) => { + console.warn("Register: failed to load preferred language", error); + }); + }, []); + + const filteredCountries = useMemo(() => { + const query = countrySearch.trim().toLowerCase(); + if (!query) { + return COUNTRIES; + } + return COUNTRIES.filter((country) => + country.name.toLowerCase().includes(query) + ); + }, [countrySearch]); + + const isFormValid = + email.trim().length > 0 && + firstName.trim().length > 0 && + lastName.trim().length > 0 && + selectedCountry; + + const handleSelectCountry = (country) => { + setSelectedCountry(country); + setIsCountryModalVisible(false); + setCountrySearch(""); + }; + + const handleCloseModal = () => { + setIsCountryModalVisible(false); + setCountrySearch(""); + }; return ( { headerType="NAVIGATION" title="Inscription" > - - - - - Saisis ton adresse mail pour commencer - - + + + + Renseigne tes informations + {preferredLanguage && ( + + Langue choisie :{" "} + + {preferredLanguage === "fr" ? "Français" : "English"} + + + )} + + + + + setIsCountryModalVisible(true)} + > + + {selectedCountry + ? selectedCountry.name + : "Choisis ton pays"} + + + + + navigate(Routes.CreatePassword, { email })} - disabled={!email} + onPress={() => + navigate(Routes.CreatePassword, { + email: email.trim(), + firstName: firstName.trim(), + lastName: lastName.trim(), + country: selectedCountry, + preferredLanguage, + }) + } + disabled={!isFormValid} /> - - + + En t’inscrivant, tu acceptes nos conditions générales{"\n"} d’utilisation et notre politique de confidentialité. navigate(Routes.Login)}> - + Tu as déjà un compte?{" "} - - Se connecter - + Se connecter + + + + + Choisis ton pays + + Fermer + + + + item.code} + renderItem={({ item }) => ( + handleSelectCountry(item)} + > + {item.name} + + )} + keyboardShouldPersistTaps="handled" + showsVerticalScrollIndicator={false} + ListEmptyComponent={ + Aucun pays trouvé + } + /> + + + ); }; export default Register; + +const styles = StyleSheet.create({ + pageContent: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + formContent: { + gap: 32, + paddingTop: 5, + paddingHorizontal: 5, + }, + title: { + fontSize: 22, + color: Palette.white, + fontFamily: FONT_FAMILY.InterSemiBold, + }, + languageInfo: { + fontSize: 14, + color: Palette.gray, + fontFamily: FONT_FAMILY.InterRegular, + }, + languageInfoValue: { + color: Palette.white, + fontFamily: FONT_FAMILY.InterSemiBold, + }, + countryButton: { + borderRadius: 12, + borderWidth: 1, + borderColor: Palette.ultraLightWhite, + backgroundColor: Palette.glass, + minHeight: 50, + justifyContent: "center", + paddingHorizontal: 16, + }, + countryButtonFilled: { + borderColor: Palette.primary, + }, + countryButtonText: { + fontSize: 14, + color: Palette.white, + fontFamily: FONT_FAMILY.InterRegular, + }, + countryButtonPlaceholder: { + color: Palette.gray, + }, + footer: { + alignItems: "center", + gap: 4, + }, + terms: { + fontSize: 12, + color: Palette.gray, + fontFamily: FONT_FAMILY.HelveticaNeueRegular, + textAlign: "center", + }, + loginText: { + fontSize: 14, + color: Palette.white, + fontFamily: FONT_FAMILY.HelveticaNeueRegular, + }, + loginLink: { + fontFamily: FONT_FAMILY.InterSemiBold, + }, + modalOverlay: { + flex: 1, + backgroundColor: Palette.transparentBlack, + justifyContent: "center", + alignItems: "center", + padding: 16, + }, + modalContainer: { + width: "100%", + maxWidth: 420, + maxHeight: "80%", + backgroundColor: Palette.darkPurple, + borderRadius: 20, + padding: 16, + gap: 12, + }, + modalHeader: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + modalTitle: { + fontSize: 18, + color: Palette.white, + fontFamily: FONT_FAMILY.InterSemiBold, + }, + closeText: { + fontSize: 14, + color: Palette.white, + fontFamily: FONT_FAMILY.InterRegular, + }, + searchInput: { + borderRadius: 12, + borderWidth: 1, + borderColor: Palette.ultraLightWhite, + paddingHorizontal: 12, + paddingVertical: 10, + color: Palette.white, + fontFamily: FONT_FAMILY.InterRegular, + }, + countryItem: { + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: Palette.ultraLightWhite, + }, + countryItemText: { + fontSize: 14, + color: Palette.white, + fontFamily: FONT_FAMILY.InterRegular, + }, + emptyListText: { + fontSize: 14, + color: Palette.gray, + fontFamily: FONT_FAMILY.InterRegular, + textAlign: "center", + paddingVertical: 20, + }, +});