From 51b02065ee10692471bbf6308342ad60b59da25a Mon Sep 17 00:00:00 2001 From: Netcup Gituser Date: Sun, 10 Dec 2023 23:18:46 +0100 Subject: [PATCH] finshed sign up --- babel.config.js | 17 +- package-lock.json | 9 + package.json | 1 + src/App.tsx | 4 +- src/components/MyButton.tsx | 1 + src/components/MyInput.tsx | 119 ++++++-- src/components/MyToast.tsx | 37 ++- src/components/map/types.ts | 5 +- src/configs/appNonSaveVar.ts | 7 +- src/configs/appNonSaveVarReducer.ts | 12 +- src/configs/types.ts | 17 +- src/event/EventManager.ts | 331 +++++++++++++++++++++++ src/event/types.ts | 71 +++++ src/helper/request.ts | 13 +- src/helper/storage/bdm/migration.ts | 4 + src/helper/storage/bdm/schemas.ts | 3 +- src/helper/storage/bdm/schemas/events.ts | 103 +++++++ src/helper/storage/bdm/types.ts | 6 +- src/lang/default.ts | 6 + src/lang/en.ts | 19 +- src/navigation/tabs/main/MapTab.tsx | 14 +- src/pages/event/EventPage.tsx | 7 + src/pages/map/map.tsx | 3 +- src/pages/welcome/login/login.tsx | 13 +- src/pages/welcome/signUp/signUp.tsx | 226 +++++++++++++++- src/user/UserManager.ts | 6 +- tsconfig.json | 8 +- 27 files changed, 983 insertions(+), 79 deletions(-) create mode 100644 src/event/EventManager.ts create mode 100644 src/event/types.ts create mode 100644 src/helper/storage/bdm/schemas/events.ts create mode 100644 src/pages/event/EventPage.tsx diff --git a/babel.config.js b/babel.config.js index 6cc5823..c35ae46 100644 --- a/babel.config.js +++ b/babel.config.js @@ -5,10 +5,20 @@ module.exports = { [ 'module-resolver', { - extensions: ['.ios.js', '.android.js', '.ios.jsx', '.android.jsx', '.js', '.jsx', '.json', '.ts', '.tsx'], + extensions: [ + '.ios.js', + '.android.js', + '.ios.jsx', + '.android.jsx', + '.js', + '.jsx', + '.json', + '.ts', + '.tsx', + ], root: ['.'], alias: { - "@redux": "./src/redux", + '@redux': './src/redux', '@lang': './src/lang', '@pages': './src/pages', '@api': './src/api', @@ -20,7 +30,8 @@ module.exports = { '@navigation': './src/navigation', '@configs': './src/configs', '@helper': './src/helper', - '@user': './src/user' + '@user': './src/user', + '@event': './src/event', }, }, ], diff --git a/package-lock.json b/package-lock.json index e24ae7b..286b706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "react-native-user-agent": "^2.3.1", "react-native-vector-icons": "^10.0.2", "react-redux": "^8.1.3", + "react-string-replace": "^1.1.1", "realm": "^12.3.1", "redux": "^4.2.1" }, @@ -19494,6 +19495,14 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/react-string-replace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.1.tgz", + "integrity": "sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/react-test-renderer": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", diff --git a/package.json b/package.json index 231743f..cf27f34 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-native-user-agent": "^2.3.1", "react-native-vector-icons": "^10.0.2", "react-redux": "^8.1.3", + "react-string-replace": "^1.1.1", "realm": "^12.3.1", "redux": "^4.2.1" }, diff --git a/src/App.tsx b/src/App.tsx index fe185c6..772bcf3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,8 @@ import configDarkTheme from '@configs/colors'; import Navigation from '@navigation/navigation'; import {MyStatusBar} from '@components/MyStatusBar'; import {SafeAreaView} from 'react-native'; +import {appVarActions} from '@configs/appVarReducer'; +import {saveVarChanges} from '@helper/appData'; const App = () => { useEffect(() => { @@ -52,7 +54,7 @@ const OtherProviders = () => { return ( - + diff --git a/src/components/MyButton.tsx b/src/components/MyButton.tsx index c65d963..9027bbe 100644 --- a/src/components/MyButton.tsx +++ b/src/components/MyButton.tsx @@ -120,6 +120,7 @@ export function MyButton({ alignItems: 'center', padding: 10, borderRadius: 10, + opacity: disabled ? currentTheme.opacity[60] : 1, }}> diff --git a/src/components/MyInput.tsx b/src/components/MyInput.tsx index 25bf403..d8c4625 100644 --- a/src/components/MyInput.tsx +++ b/src/components/MyInput.tsx @@ -1,9 +1,15 @@ -import {KeyboardTypeOptions, TextInput, View} from 'react-native'; +import { + KeyboardTypeOptions, + StyleSheet, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; import {MyIcon} from './MyIcon'; import {useSelector} from 'react-redux'; import {RootState} from '@redux/store'; import {Text} from '@gluestack-ui/themed'; -import {useRef, useState} from 'react'; +import {ReactNode, useRef, useState} from 'react'; interface MyIconInputProps { text: string; @@ -13,6 +19,9 @@ interface MyIconInputProps { value?: string; onChangeText?: (text: string) => void; disableContainer?: boolean; + maxLength?: number; + rightComponent?: ReactNode; + helper?: ReactNode; } export function MyIconInput({ @@ -23,42 +32,106 @@ export function MyIconInput({ value, onChangeText, disableContainer, + maxLength, + rightComponent, + helper, }: MyIconInputProps) { const currentTheme = useSelector( (state: RootState) => state.nonSaveVariables.theme.colors, ); + const [password, setPassword] = useState(''); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + + const togglePasswordVisibility = () => { + setIsPasswordVisible(!isPasswordVisible); + }; + return ( - - + + + + + + + + {text} + + + + + + {rightComponent && ( + {rightComponent} + )} + + - - {text} - - + {helper} + + ); +} + +export interface MyInputErrorProps { + iconName?: string; + text: string; +} + +export function MyInputError({ + iconName, + text, +}: MyInputErrorProps): React.ReactElement { + const theme = useSelector( + (state: RootState) => state.nonSaveVariables.theme.colors, + ); + + return ( + + + + {text} ); } diff --git a/src/components/MyToast.tsx b/src/components/MyToast.tsx index 316c030..d57ae29 100644 --- a/src/components/MyToast.tsx +++ b/src/components/MyToast.tsx @@ -1,10 +1,20 @@ -import {Alert, CloseIcon, useToast} from '@gluestack-ui/themed'; +import { + Alert, + CloseIcon, + InfoIcon, + Toast, + ToastDescription, + ToastTitle, + useToast, +} from '@gluestack-ui/themed'; import {HStack} from '@gluestack-ui/themed'; import {Text} from '@gluestack-ui/themed'; import {VStack} from '@gluestack-ui/themed'; import {AlertIcon} from '@gluestack-ui/themed'; -import {MyButton} from './MyButton'; +import {MyButton, MyIconButton} from './MyButton'; +import {AlertText} from '@gluestack-ui/themed'; +/* const Toast = () => { const toast = useToast(); const ToastDetails = [ @@ -37,7 +47,7 @@ const Toast = () => { description: 'Please enter a valid email address', }, ]; -}; +}; */ interface toastType { id?: string; @@ -46,7 +56,7 @@ interface toastType { title: any; description: any; isClosable?: boolean; - rest: any; + rest?: any; } interface alertType extends toastType { @@ -64,9 +74,10 @@ function showToast(toast: any, item: toastType) { rest, }: alertType) => ( @@ -85,15 +96,17 @@ function showToast(toast: any, item: toastType) { ? '$white' : variant !== 'outline' ? '$black' - : null + : '$white' }> {title} {isClosable ? ( - toast.close(id)} /> ) : /* {description} diff --git a/src/components/map/types.ts b/src/components/map/types.ts index 6afe2e6..0b765e2 100644 --- a/src/components/map/types.ts +++ b/src/components/map/types.ts @@ -1,9 +1,12 @@ -type EventID = string; +import { EventID, EventType } from '@event/types'; + + interface BasicEvent { id: EventID; latitude: number; longitude: number; + type: EventType | "cluster"; } interface PA_Point_Cluster extends BasicEvent { diff --git a/src/configs/appNonSaveVar.ts b/src/configs/appNonSaveVar.ts index d05f213..b095b11 100644 --- a/src/configs/appNonSaveVar.ts +++ b/src/configs/appNonSaveVar.ts @@ -6,12 +6,15 @@ import {AccountName} from './types'; import {getVersionByNum, VersionType} from '@helper/version'; import configDarkTheme, {ThemeTokensType} from '@configs/colors'; +import { EventID, PAEvent } from '@event/types'; +import { PA_Point } from '@components/map/types'; + -import {PA_Point} from '@components/map/cluster/getData'; export const APP_VERSION = getVersionByNum(1); export const AppVarMaxBackups: number = 10; export const maxCachedUsers = 30; +export const maxCachedEvents = 30; export enum appStatus { IS_LOADING, @@ -31,6 +34,7 @@ export interface NON_SAVE_VARS { theme: ThemeTokensType; connectionStatus: connectionStatus; cachedUsers: {[key: AccountName]: User}; + cachedEvents: {[key: EventID]: PAEvent}; chats: {[key: roomId]: chatEntity}; chatActivity: roomId[]; selectedChat: roomId | 'none'; @@ -43,6 +47,7 @@ export const non_save_vars: NON_SAVE_VARS = { theme: configDarkTheme.tokens, connectionStatus: connectionStatus.UNKNOWN, cachedUsers: {}, + cachedEvents: {}, chats: {}, chatActivity: [], selectedChat: 'none', diff --git a/src/configs/appNonSaveVarReducer.ts b/src/configs/appNonSaveVarReducer.ts index d6b5679..8b3e202 100644 --- a/src/configs/appNonSaveVarReducer.ts +++ b/src/configs/appNonSaveVarReducer.ts @@ -5,8 +5,9 @@ import {ThemeTokensType} from '@configs/colors'; import {chatEntity, roomId} from '@configs/chat/types'; import {User} from '@user/types'; -import {AccountName} from './types'; -import {PA_Point} from '@components/map/cluster/getData'; +import {AccountName, EventId} from './types'; +import {PA_Point} from '@components/map/types'; +import {PAEvent} from '@event/types'; export const appNonSaveVariablesSlice = createSlice({ name: 'non_save_vars', @@ -24,6 +25,13 @@ export const appNonSaveVariablesSlice = createSlice({ removeCachedUser: (state, action: PayloadAction) => { delete state.cachedUsers[action.payload]; }, + setCachedEvent: (state, action: PayloadAction) => { + state.cachedEvents[action.payload.UUID] = action.payload; + }, + removeCachedEvent: (state, action: PayloadAction) => { + delete state.cachedEvents[action.payload]; + }, + setSelectedChat: (state, action: PayloadAction) => { state.selectedChat = action.payload; }, diff --git a/src/configs/types.ts b/src/configs/types.ts index aaf9c1c..390f2eb 100644 --- a/src/configs/types.ts +++ b/src/configs/types.ts @@ -17,11 +17,14 @@ export type XAuthorization = string; //export type UserId = string; //export type WebSocketSessionId = string; +export type EventId = string; + export const accountNameOptions = { minLength: 4, maxLength: 24, isAllowed: (text: string): boolean => { - return text.match('^[a-zA-Z0-9_.]+$') !== null; + // allows usernames that start and end with a lowercase letter or digit, with optional dots or underscores in the middle, and it is case-insensitive + return text.match(/^[a-z0-9](?:[._]*[a-z0-9])*$/i) !== null; }, }; @@ -38,7 +41,17 @@ export const passwordOptions = { maxLength: 64, minBits: 50, isAllowed: (text: string): boolean => { - return /\W/.test(text) && /[a-zA-Z]/.test(text) && /[a-zA-Z]/.test(text); + // return /\W/.test(text) && /[a-zA-Z]/.test(text) && /[a-zA-Z]/.test(text); + + /* + Contains at least one uppercase letter + Contains at least one lowercase letter + Contains at least one digit (number) + Contains at least one special character + */ + return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+={}\[\]|;:'",.<>\/?]).*$/.test( + text, + ); }, }; diff --git a/src/event/EventManager.ts b/src/event/EventManager.ts new file mode 100644 index 0000000..c5c524a --- /dev/null +++ b/src/event/EventManager.ts @@ -0,0 +1,331 @@ +import {maxCachedEvents} from '@configs/appNonSaveVar'; +import {appNonSaveVarActions} from '@configs/appNonSaveVarReducer'; +import {AccountName, XAuthorization} from '@configs/types'; +import {makeRequest, apiBackendRequest} from '@helper/request'; +import BigDataManager from '@helper/storage/BigDataManager'; +import {RootState, store} from '@redux/store'; +import {useSelector} from 'react-redux'; +import { + BasicEventProp, + createEventProp, + EventBannerPicture, + EventBannerPictureType, + EventID, + EventType, + NumToEventType, + PAEvent, +} from './types'; + +import {SourceProp} from '@user/types'; + +let cachedEventList: EventID[] = []; + +async function getEvent( + UUID: EventID, + save?: boolean, +): Promise { + if (UUID === 'none') { + return undefined; + } + + let event: PAEvent | undefined; + + let state = store.getState(); + let eventIsInCache = false; + + { + const eve = state.nonSaveVariables.cachedEvents[UUID]; + if (eve !== undefined) { + event = eve; + eventIsInCache = true; + } + } + + + if (event === undefined) { + const eveDBKeys = BigDataManager.databases.events.keys; + const eve = await BigDataManager.databases.events.getEntry(UUID); + + if (eve !== undefined && eve !== null) { + let EventBannerPicture: EventBannerPicture = { + lq: + eve[eveDBKeys.EventBannerPictureBinaryLQ].byteLength !== 0 + ? createEventProp( + SourceProp.offline, + //Buffer.from(eve[eveDBKeys.EventBannerPictureBinaryLQ]), + new Blob([eve[eveDBKeys.EventBannerPictureBinaryLQ] as unknown as string]), + ) + : createEventProp( + SourceProp.online, + undefined, + eve[eveDBKeys.EventBannerPicture], + ), + hq: + eve[eveDBKeys.EventBannerPictureBinaryHQ].byteLength !== 0 + ? createEventProp( + SourceProp.offline, + //Buffer.from(eve[eveDBKeys.EventBannerPictureBinaryHQ]), + new Blob([eve[eveDBKeys.EventBannerPictureBinaryHQ] as unknown as string]), + ) + : createEventProp( + SourceProp.online, + undefined, + eve[eveDBKeys.EventBannerPicture], + ), + }; + + event = { + UUID, + Name: createEventProp(SourceProp.offline, eve[eveDBKeys.Name]), + Description: createEventProp( + SourceProp.offline, + eve[eveDBKeys.Description], + ), + StartDate: createEventProp( + SourceProp.offline, + eve[eveDBKeys.StartDate], + ), + EndDate: createEventProp(SourceProp.offline, eve[eveDBKeys.EndDate]), + Latitude: createEventProp(SourceProp.offline, eve[eveDBKeys.Latitude]), + Longitude: createEventProp( + SourceProp.offline, + eve[eveDBKeys.Longitude], + ), + Type: createEventProp(SourceProp.offline, NumToEventType(eve[eveDBKeys.Type])), + Theme: createEventProp(SourceProp.offline, eve[eveDBKeys.Theme]), + FriendList: createEventProp( + SourceProp.offline, + eve[eveDBKeys.FriendList], + ), + UserLength: createEventProp( + SourceProp.offline, + eve[eveDBKeys.UserLength], + ), + ButtonAction: createEventProp( + SourceProp.offline, + eve[eveDBKeys.ButtonAction], + ), + lastUpdateTimestamp: eve[eveDBKeys.lastUpdateTimestamp], + EventBannerPicture, + }; + } + } + + if (event === undefined) { + try { + /*const resp = await makeRequest({ + path: apiBackendRequest.GET_USER_PROFILE, + requestGET: {':UUID': UUID}, + response: { + Description: '', + FollowersCount: 0, + FollowingCount: 0, + Eventname: '', + XpLevel: 0, + XpPoints: 0, + AvatarUrl: '', + }, + });*/ + + /* Name: BasicEventProp; + Description: BasicEventProp; + StartDate: BasicEventProp; + EndDate: BasicEventProp; + Latitude: BasicEventProp; + Longitude: BasicEventProp; + Type: BasicEventProp; + Theme: BasicEventProp; + lastUpdateTimestamp: timestamp; + FriendList: BasicEventProp; + UserLength: BasicEventProp; + EventBannerPicture: EventBannerPicture; + ButtonAction: BasicEventProp; */ + + const resp = {response:{ + Name: 'test event', + Description: 'test description', + StartDate: 1702230005, + EndDate: new Date().getTime() + 100000, + Latitude: 48.7758, + Longitude: 9.1829, + Type: 0, + Theme: 0, + FriendList: [], + UserLength: 5, + pic: 'https://www.w3schools.com/w3css/img_lights.jpg', + ButtonAction: '{"url":"https://www.w3schools.com/w3css/img_lights.jpg"}', + }}; + + event = { + UUID, + Description: createEventProp( + SourceProp.cached, + resp.response.Description, + ), + lastUpdateTimestamp: Math.floor(new Date().getTime() / 1000), + EventBannerPicture: { + lq: createEventProp( + SourceProp.online, + undefined, + resp.response.pic + "?type=low", + ), + hq: createEventProp( + SourceProp.online, + undefined, + resp.response.pic, + ), + }, + Name: createEventProp( + SourceProp.cached, + resp.response.Name, + ), + StartDate: createEventProp( + SourceProp.cached, + resp.response.StartDate, + ), + EndDate: createEventProp( + SourceProp.cached, + resp.response.EndDate, + ), + Latitude: createEventProp( + SourceProp.cached, + resp.response.Latitude, + ), + Longitude: createEventProp( + SourceProp.cached, + resp.response.Longitude, + ), + Type: createEventProp( + SourceProp.cached, + NumToEventType(resp.response.Type), + ), + Theme: createEventProp( + SourceProp.cached, + resp.response.Theme, + ), + FriendList: createEventProp( + SourceProp.cached, + resp.response.FriendList, + ), + UserLength: createEventProp( + SourceProp.cached, + resp.response.UserLength, + ), + ButtonAction: createEventProp( + SourceProp.cached, + resp.response.ButtonAction, + ), + + }; + + //BigDataManager.setEntry('events', event); + } catch (error: any) { + console.error(error.status); + } + } + + if (eventIsInCache === false && event !== undefined) { + console.log('save in cache'); + store.dispatch(appNonSaveVarActions.setCachedEvent(event)); + cachedEventList.push(event.UUID); + + if (cachedEventList.length > maxCachedEvents) { + let eveId = cachedEventList[0]; + cachedEventList.shift(); + console.log('eveId', eveId); + + store.dispatch(appNonSaveVarActions.removeCachedEvent(eveId)); + } + } + + return event; +} + +enum GetParam { + CACHE = 0, + SAVE, +} + +let getEventList: {[key: AccountName]: GetParam} = {}; + +async function refreshEvents() { + for (let UUID in getEventList) { + const param = getEventList[UUID]; + delete getEventList[UUID]; + + await getEvent(UUID); + } +} + +setInterval(refreshEvents, 500); + +function addEventToGetQueue(UUID: EventID, param: GetParam) { + if (getEventList[UUID] === undefined) { + getEventList[UUID] = param; + } else if (getEventList[UUID] < param) { + getEventList[UUID] = param; + } +} + +function getEventSelector(UUID: EventID) { + addEventToGetQueue(UUID, GetParam.CACHE); + + const myEvent = useSelector( + (state: RootState) => state.nonSaveVariables.cachedEvents[UUID], + ); + + if (myEvent === undefined) { + return initUndefinedEvent(UUID); + } + + return myEvent; +} + +function getEventSelectorPicture(UUID: EventID): EventBannerPicture { + addEventToGetQueue(UUID, GetParam.CACHE); + + const myEvent = useSelector( + (state: RootState) => + state.nonSaveVariables.cachedEvents[UUID]?.EventBannerPicture, + ); + + if (myEvent === undefined) { + return { + lq: createEventProp(SourceProp.online), + hq: createEventProp(SourceProp.online), + }; + } + + return myEvent; +} + +function initUndefinedEvent(UUID: EventID): PAEvent { + return { + UUID, + Name: createEventProp(SourceProp.online), + Description: createEventProp(SourceProp.online), + StartDate: createEventProp(SourceProp.online), + EndDate: createEventProp(SourceProp.online), + Latitude: createEventProp(SourceProp.online), + Longitude: createEventProp(SourceProp.online), + Type: createEventProp(SourceProp.online), + Theme: createEventProp(SourceProp.online), + lastUpdateTimestamp: 0, + FriendList: createEventProp(SourceProp.online), + UserLength: createEventProp(SourceProp.online), + EventBannerPicture: { + lq: createEventProp(SourceProp.online), + hq: createEventProp(SourceProp.online), + }, + ButtonAction: createEventProp(SourceProp.online), + }; + } + + +const EventManager = { + getEvent, + getEventSelector, + getEventSelectorPicture, + initUndefinedEvent, +}; +export default EventManager; diff --git a/src/event/types.ts b/src/event/types.ts new file mode 100644 index 0000000..35cfa87 --- /dev/null +++ b/src/event/types.ts @@ -0,0 +1,71 @@ +import {SourceProp} from '@user/types'; + +import {EventId, timestamp} from '@configs/types'; + +export type EventID = string; +export type EventType = 'event' | 'eventStore'; + +export enum EventThemes { + default = 0, + small = 1, +} + +export interface BasicEventProp { + source: SourceProp; + url?: string; + data?: T1; +} + +export function createEventProp( + source: SourceProp, + data?: T1, + url?: string, +): BasicEventProp { + return {source, data, url}; +} + +export type EventBannerPictureType = BasicEventProp; + +export interface EventBannerPicture { + lq: EventBannerPictureType; //low quality + hq?: EventBannerPictureType; //high quality +} + +export interface PAEvent { + UUID: EventId; + Name: BasicEventProp; + Description: BasicEventProp; + StartDate: BasicEventProp; + EndDate: BasicEventProp; + Latitude: BasicEventProp; + Longitude: BasicEventProp; + Type: BasicEventProp; + Theme: BasicEventProp; + lastUpdateTimestamp: timestamp; + FriendList: BasicEventProp; + UserLength: BasicEventProp; + EventBannerPicture: EventBannerPicture; + ButtonAction: BasicEventProp; +} + +export function NumToEventType(num: number): EventType { + switch (num) { + case 0: + return 'event'; + case 1: + return 'eventStore'; + default: + return 'event'; + } +} + +export function EventTypeToNum(type: EventType): number { + switch (type) { + case 'event': + return 0; + case 'eventStore': + return 1; + default: + return 0; + } +} \ No newline at end of file diff --git a/src/helper/request.ts b/src/helper/request.ts index 4ad93bc..4709059 100644 --- a/src/helper/request.ts +++ b/src/helper/request.ts @@ -23,6 +23,7 @@ export enum apiBackendRequest { GET_USER_PROFILE = '/users/:accountName', LOGOUT = '/user/logout', SIGN_UP = '/user/signup', + CHECK_ACCOUNT_NAME = '/user/check/:accountName', /*REGISTER_STEP_1 = '/admin/users/email', REGISTER_RESEND_MAIL = '/admin/users/email/resend', REGISTER_STEP_2 = '/verify/email/:xToken/:verifyId', @@ -147,6 +148,15 @@ interface GET_USER_PROFILE extends defaultRequest { }; } +interface CHECK_ACCOUNT_NAME extends defaultRequest { + path: apiBackendRequest.CHECK_ACCOUNT_NAME; + + requestGET: { + ':accountName': AccountName; + }; + response: {}; +} + interface APP_START extends defaultRequest { path: apiBackendRequest.APP_START; @@ -180,7 +190,8 @@ type FetchTypes = | LOGIN*/ | GET_USER_PROFILE | APP_START - | LOGOUT; + | LOGOUT + | CHECK_ACCOUNT_NAME; /* function isA(obj: any): obj is REGISTER_STEP_1 { return obj.request !== undefined; diff --git a/src/helper/storage/bdm/migration.ts b/src/helper/storage/bdm/migration.ts index f0c9f13..235b634 100644 --- a/src/helper/storage/bdm/migration.ts +++ b/src/helper/storage/bdm/migration.ts @@ -40,6 +40,10 @@ export const DBMigration: {[key in databaseNames]: any} = { chatRoomInfos: (Schema: typeof DBSchemas.chatRoomInfos) => { const callback: MigrationCallback = (oldRealm, newRealm) => {}; + return callback; + },events: (Schema: typeof DBSchemas.events) => { + const callback: MigrationCallback = (oldRealm, newRealm) => {}; + return callback; }, }; diff --git a/src/helper/storage/bdm/schemas.ts b/src/helper/storage/bdm/schemas.ts index d836b2d..9736c4c 100644 --- a/src/helper/storage/bdm/schemas.ts +++ b/src/helper/storage/bdm/schemas.ts @@ -1,8 +1,9 @@ import chat from './schemas/chat'; import users from './schemas/users'; import chatRoomInfos from './schemas/chatRoomInfos'; +import events from './schemas/events'; -const DBSchemas = {users, chat, chatRoomInfos}; +const DBSchemas = {users, chat, chatRoomInfos, events}; export const SkipDBSchemas = [chat.details.name]; export type databaseConfType = typeof DBSchemas[keyof typeof DBSchemas]; diff --git a/src/helper/storage/bdm/schemas/events.ts b/src/helper/storage/bdm/schemas/events.ts new file mode 100644 index 0000000..da8da44 --- /dev/null +++ b/src/helper/storage/bdm/schemas/events.ts @@ -0,0 +1,103 @@ +import {filterParam, getAllEntries, getEntry} from '../get'; +import {DBMigration} from '../migration'; +import {setEntry} from '../set'; + +import {databaseConf, possibleDBKeys} from '../types'; + +enum keys { + UUID = 'a', + Name = 'b', + Description = 'c', + StartDate = 'd', + EndDate = 'e', + Latitude = 'f', + Longitude = 'g', + Type = 'h', + Theme = 'i', + lastUpdateTimestamp = 'j', + FriendList = 'k', + UserLength = 'l', + EventBannerPicture = 'm', //URL + EventBannerPictureBinaryLQ = 'n', + EventBannerPictureBinaryHQ = 'o', + ButtonAction = 'p', +} + +const name = 'events'; +const primaryKey: keyof typeof propsDefault = keys.UUID; + +const propsType: {[key in keyof typeof propsDefault]: string} = { + [keys.UUID]: 'string', + [keys.Name]: 'string', + [keys.Description]: 'string', + [keys.StartDate]: 'int', + [keys.EndDate]: 'int', + [keys.Latitude]: 'double', + [keys.Longitude]: 'double', + [keys.Type]: 'int', + [keys.Theme]: 'int', + [keys.lastUpdateTimestamp]: 'int', + [keys.FriendList]: 'string[]', + [keys.UserLength]: 'int', + [keys.EventBannerPicture]: 'string', //URL + [keys.EventBannerPictureBinaryLQ]: 'data', + [keys.EventBannerPictureBinaryHQ]: 'data', + [keys.ButtonAction]: 'string', +}; + +const propsDefault = { + [keys.UUID]: '', + [keys.Name]: '', + [keys.Description]: '', + [keys.StartDate]: 0, + [keys.EndDate]: 0, + [keys.Latitude]: 0, + [keys.Longitude]: 0, + [keys.Type]: 0, + [keys.Theme]: 0, + [keys.lastUpdateTimestamp]: 0, + [keys.FriendList]: ['none'], + [keys.UserLength]: 0, + [keys.EventBannerPicture]: '', //URL + [keys.EventBannerPictureBinaryLQ]: new ArrayBuffer(0), + [keys.EventBannerPictureBinaryHQ]: new ArrayBuffer(0), + [keys.ButtonAction]: '', +}; + +const thisSchema: databaseConf = { + filePath: name, + version: 1, + keys, + migration: () => { + return DBMigration[name](thisSchema); + }, + setEntry: (val: typeof thisSchema.defaultProps, suffix?: string) => { + return setEntry( + thisSchema, + val, + suffix, + ); + }, + getEntry: (key: possibleDBKeys, suffix?: string) => { + return getEntry( + thisSchema, + key, + suffix, + ); + }, + getAllEntries: (filter?: filterParam, suffix?: string) => { + return getAllEntries( + thisSchema, + filter, + suffix, + ); + }, + defaultProps: propsDefault, + details: { + name, + properties: propsType, + primaryKey, + }, +}; + +export default thisSchema; diff --git a/src/helper/storage/bdm/types.ts b/src/helper/storage/bdm/types.ts index 766d1ec..b71d848 100644 --- a/src/helper/storage/bdm/types.ts +++ b/src/helper/storage/bdm/types.ts @@ -1,7 +1,7 @@ import MyUserManager from '@user/MyUserManager'; import {filterParam} from './get'; -export type databaseNames = 'users' | 'chat' | 'chatRoomInfos'; +export type databaseNames = 'users' | 'chat' | 'chatRoomInfos' | 'events'; export type possibleDBKeys = string; export interface databaseConf { @@ -31,10 +31,10 @@ export interface databaseNameSuffix { export function mergeDBName(nameObj: databaseNameSuffix, web?: 'web'): string { if (web === 'web') { return nameObj.suffix === undefined - ? nameObj.name + '-' + MyUserManager.getSelectedUserId() + ? nameObj.name + '-' + MyUserManager.getSelectedUserAccount() : nameObj.name + '-' + - MyUserManager.getSelectedUserId() + + MyUserManager.getSelectedUserAccount() + ('_' + nameObj.suffix); } diff --git a/src/lang/default.ts b/src/lang/default.ts index 508657d..28261bf 100644 --- a/src/lang/default.ts +++ b/src/lang/default.ts @@ -65,6 +65,7 @@ export default interface LangFormat { title: string; description: string; inputUsername: string; + error: string; }; signUpStepPhoneNumber: { title: string; @@ -79,12 +80,17 @@ export default interface LangFormat { title: string; description: string; inputPassword: string; + errorLength: string; + errorPasswordInvalid: string; }; signUpStepAccountName: { title: string; description: string; inputAccountName: string; buttonGetStarted: string; + signUpError: {[key: number]: string}; + errorLength: string; + errorAccountNameInvalid: string; }; }; profile: { diff --git a/src/lang/en.ts b/src/lang/en.ts index 61130fd..ad2d530 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -65,6 +65,7 @@ export const lang: LangFormat = { title: "Let's get started, what's your name?", description: 'The name will be displayed on your profil overview', inputUsername: 'Username', + error: 'At least ${minLength} characters are required', }, signUpStepPhoneNumber: { title: 'Create your account using your phone number', @@ -77,8 +78,11 @@ export const lang: LangFormat = { }, signUpStepPassword: { title: "You'll need a password", - description: 'Make sure it’s 8 characters or more.', + description: 'Make sure it’s ${minLength} characters or more.', inputPassword: 'PASSWORD', + errorLength: 'Password must be at least ${minLength} characters long', + errorPasswordInvalid: + 'Must include at least on uppercase letter, one lowercase letter, one number and one special character', }, signUpStepAccountName: { title: 'Next, create your account name', @@ -86,6 +90,15 @@ export const lang: LangFormat = { 'Your account name is unique and is used for friends to find you.', inputAccountName: 'ACCOUNT NAME', buttonGetStarted: 'Get Started', + errorLength: 'Account name must be at least ${minLength} characters long', + errorAccountNameInvalid: + 'Account name can only contain \n20a-z, 0-9, underscores and dots', + signUpError: { + 400: 'Invalid account name', + 401: 'Invalid credentials', + 500: 'Server error', + 502: 'Server not reachable', + }, }, }, profile: { @@ -119,13 +132,13 @@ export const lang: LangFormat = { changeUsername: { username: 'USERNAME', info: 'You can use a-z, 0-9 and underscores.', - info2: 'Minimum length is 3 characters.', + info2: 'Minimum length is ${minLength} characters.', }, changePassword: { currentPassword: 'CURRENT PASSWORD', newPassword: 'NEW PASSWORD', repeatNewPassword: 'REPEAT NEW PASSWORD', - info: 'Make sure it’s 8 characters or more.', + info: 'Make sure it’s ${minLength} characters or more.', info2: 'You will be logged out after changing your password.', }, help: { diff --git a/src/navigation/tabs/main/MapTab.tsx b/src/navigation/tabs/main/MapTab.tsx index 843379e..a54715c 100644 --- a/src/navigation/tabs/main/MapTab.tsx +++ b/src/navigation/tabs/main/MapTab.tsx @@ -11,6 +11,7 @@ import {useSelector} from 'react-redux'; import {Map} from '@pages/map/map'; import {EventID} from '@components/map/types'; +import EventPage from '@pages/event/EventPage'; export const MapTabName = 'Map'; @@ -47,13 +48,20 @@ function MapTab() { /> ); diff --git a/src/pages/event/EventPage.tsx b/src/pages/event/EventPage.tsx new file mode 100644 index 0000000..ec0eaa1 --- /dev/null +++ b/src/pages/event/EventPage.tsx @@ -0,0 +1,7 @@ +import {Text} from '@gluestack-ui/themed'; + +function EventPage() { + return EventPage; +} + +export default EventPage; diff --git a/src/pages/map/map.tsx b/src/pages/map/map.tsx index 299a16a..d5d5547 100644 --- a/src/pages/map/map.tsx +++ b/src/pages/map/map.tsx @@ -5,7 +5,8 @@ import React, {useState} from 'react'; // Add useState import import DisplayMarkerList from '@components/map/DisplayMarkerList'; -import getLocationData, {PA_Point} from '@components/map/cluster/getData'; +import getLocationData from '@components/map/cluster/getData'; +import {PA_Point} from '@components/map/types'; import {store} from '@redux/store'; import {appNonSaveVarActions} from '@configs/appNonSaveVarReducer'; import {Position} from '@rnmapbox/maps/src/types/Position'; diff --git a/src/pages/welcome/login/login.tsx b/src/pages/welcome/login/login.tsx index 53439db..d1e3ab9 100644 --- a/src/pages/welcome/login/login.tsx +++ b/src/pages/welcome/login/login.tsx @@ -9,7 +9,7 @@ import { navigateToHome, } from '@navigation/registration/registration'; import {useNavigation} from '@react-navigation/native'; -import {RootState} from '@redux/store'; +import {RootState, store} from '@redux/store'; import MyUserManager from '@user/MyUserManager'; import {useState} from 'react'; import {View} from 'react-native'; @@ -60,8 +60,6 @@ export function Login() { text={lang.buttonLogin} style={{marginBottom: 20}} onPress={() => { - console.log('login'); - setIsLoading(true); makeRequest({ @@ -95,13 +93,18 @@ export function Login() { console.log('reason', reason); setIsLoading(false); + let text = + store.getState().appVariables.lang.registration + .signUpStepAccountName.signUpError[ + reason.status as number + ]; + showToast(toast, { title: 'Failed', variant: 'solid', action: 'error', - description: undefined, + description: text, isClosable: true, - rest: {colorScheme: 'primary'}, }); }); }} diff --git a/src/pages/welcome/signUp/signUp.tsx b/src/pages/welcome/signUp/signUp.tsx index 8aeefb8..5b4f455 100644 --- a/src/pages/welcome/signUp/signUp.tsx +++ b/src/pages/welcome/signUp/signUp.tsx @@ -1,8 +1,16 @@ -import {MyButton} from '@components/MyButton'; -import {MyIconInput} from '@components/MyInput'; +import {MyButton, MyIconButton} from '@components/MyButton'; +import {MyIcon} from '@components/MyIcon'; +import {MyIconInput, MyInputError} from '@components/MyInput'; import {MyScreenContainer} from '@components/MyScreenContainer'; import {MyTitle} from '@components/MyTitle'; +import showToast from '@components/MyToast'; import {appVarActions} from '@configs/appVarReducer'; +import { + accountNameOptions, + passwordOptions, + userNameOptions, +} from '@configs/types'; +import {Spinner, set, useToast} from '@gluestack-ui/themed'; import {ToBase64} from '@helper/base64'; import {apiBackendRequest, makeRequest} from '@helper/request'; import {RootScreenNavigationProp} from '@navigation/navigation'; @@ -13,10 +21,11 @@ import { import {useNavigation} from '@react-navigation/native'; import {RootState, store} from '@redux/store'; import MyUserManager from '@user/MyUserManager'; -import {useState} from 'react'; +import {useEffect, useState} from 'react'; import {Text} from 'react-native'; import {View} from 'react-native'; import {useSelector} from 'react-redux'; +import reactStringReplace from 'react-string-replace'; function Title({text, description}: {text: string; description?: string}) { return ( @@ -38,6 +47,17 @@ export function SignUpStepUsername() { ); const [username, setUsername] = useState(''); + const [inputTouched, setInputTouched] = useState(false); + + const usernameValid = username.length < userNameOptions.minLength; + + const errorText = reactStringReplace( + lang.signUpStepUsername.error, + '${minLength}', + (match, i) => { + return userNameOptions.minLength.toString(); + }, + ); return ( setUsername(text)} + onChangeText={text => { + setUsername(text); + setInputTouched(true); + }} + maxLength={userNameOptions.maxLength} + helper={ + inputTouched && + usernameValid && + } /> { let rp = {...registerProcess}; @@ -173,6 +202,49 @@ export function SignUpStepPassword() { ); const [password, setPassword] = useState(''); + const [inputTouched, setInputTouched] = useState(false); + + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + + const togglePasswordVisibility = () => { + setIsPasswordVisible(!isPasswordVisible); + }; + + const passwordValid = () => { + if (password.length < passwordOptions.minLength) { + return false; + } else if (!passwordOptions.isAllowed(password)) { + return false; + } else { + return true; + } + }; + + const descriptionText = reactStringReplace( + lang.signUpStepPassword.description, + '${minLength}', + () => { + return passwordOptions.minLength.toString(); + }, + ); + + const errorLengthText = reactStringReplace( + lang.signUpStepPassword.errorLength, + '${minLength}', + () => { + return passwordOptions.minLength.toString(); + }, + ); + + const errorText = () => { + if (password.length < passwordOptions.minLength) { + return errorLengthText.join(''); + } else if (!passwordOptions.isAllowed(password)) { + return lang.signUpStepPassword.errorPasswordInvalid; + } else { + return ''; + } + }; return ( <View style={{gap: 12, marginTop: 20}}> <MyIconInput text={lang.signUpStepPassword.inputPassword} iconName="lock" - secureTextEntry + secureTextEntry={!isPasswordVisible} value={password} - onChangeText={text => setPassword(text)} + maxLength={passwordOptions.maxLength} + onChangeText={text => { + setPassword(text); + setInputTouched(true); + }} + rightComponent={ + <MyIconButton + MyIconProps={{ + name: isPasswordVisible ? 'visibility-off' : 'visibility', + size: 24, + }} + onPress={togglePasswordVisibility} + /> + } + helper={ + inputTouched && + !passwordValid() && <MyInputError text={errorText()} /> + } /> <MyButton type="secondary" text={lang.buttonNext} style={{marginBottom: 2}} + disabled={!passwordValid()} onPress={() => { let rp = {...registerProcess}; @@ -216,11 +306,22 @@ export function SignUpStepPassword() { ); } +enum AccountNameAvailable { + Loading, + Available, + NotAvailable, +} + export function SignUpStepAccountName() { const lang = useSelector( (state: RootState) => state.appVariables.lang.registration, ); + const currentTheme = useSelector( + (state: RootState) => state.nonSaveVariables.theme.colors, + ); + const navigation = useNavigation<RootScreenNavigationProp>(); + const toast = useToast(); const registerProcess = useSelector( (state: RootState) => state.appVariables.preferences.RegisterProcess, @@ -228,6 +329,72 @@ export function SignUpStepAccountName() { const [isLoading, setIsLoading] = useState(false); const [accountName, setAccountName] = useState(''); + const [isAccountNameAvailable, setIsAccountNameAvailable] = useState( + AccountNameAvailable.Loading, + ); + const [inputTouched, setInputTouched] = useState(false); + + const accountNameValid = () => { + if (accountName.length < accountNameOptions.minLength) { + return false; + } else if (!accountNameOptions.isAllowed(accountName)) { + return false; + } else { + return true; + } + }; + + const errorText = () => { + if (accountName.length < accountNameOptions.minLength) { + return reactStringReplace( + lang.signUpStepAccountName.errorLength, + '${minLength}', + () => { + return accountNameOptions.minLength.toString(); + }, + ).join(''); + } else if (!accountNameOptions.isAllowed(accountName)) { + return lang.signUpStepAccountName.errorAccountNameInvalid; + } else { + return ''; + } + }; + + const rightComponent = () => { + const closeIcon = ( + <MyIcon name="close" size={24} color={currentTheme.red600} /> + ); + + if (!accountNameValid()) { + return closeIcon; + } else if (isAccountNameAvailable === AccountNameAvailable.Loading) { + return <Spinner />; + } else if (isAccountNameAvailable === AccountNameAvailable.Available) { + return <MyIcon name="check" size={24} color={currentTheme.green400} />; + } else { + return closeIcon; + } + }; + + useEffect(() => { + if (!accountNameValid()) return; + + const delay = 400; + const timeoutId = setTimeout(() => { + makeRequest({ + path: apiBackendRequest.CHECK_ACCOUNT_NAME, + requestGET: {':accountName': accountName}, + response: {}, + }) + .then(() => setIsAccountNameAvailable(AccountNameAvailable.Available)) + .catch(() => + setIsAccountNameAvailable(AccountNameAvailable.NotAvailable), + ); + }, delay); + + // Cleanup the timeout on component unmount or when inputValue changes + return () => clearTimeout(timeoutId); + }, [accountName]); return ( <MyScreenContainer @@ -246,7 +413,17 @@ export function SignUpStepAccountName() { text={lang.signUpStepAccountName.inputAccountName} iconName="person" value={accountName} - onChangeText={text => setAccountName(text)} + onChangeText={text => { + setAccountName(text); + setInputTouched(true); + setIsAccountNameAvailable(AccountNameAvailable.Loading); + }} + maxLength={accountNameOptions.maxLength} + rightComponent={rightComponent()} + helper={ + inputTouched && + !accountNameValid() && <MyInputError text={errorText()} /> + } /> <MyButton @@ -254,15 +431,19 @@ export function SignUpStepAccountName() { text={lang.signUpStepAccountName.buttonGetStarted} style={{marginBottom: 2}} isLoading={isLoading} + disabled={ + !accountNameValid() || + isAccountNameAvailable !== AccountNameAvailable.Available + } onPress={() => { + setIsLoading(true); + let rp = {...registerProcess}; rp.AccountName = accountName; store.dispatch(appVarActions.setRegisterProcess(rp)); - console.log('registerProcess', rp); - makeRequest({ path: apiBackendRequest.SIGN_UP, request: rp, @@ -273,8 +454,6 @@ export function SignUpStepAccountName() { }, }) .then(resp => { - console.log('response', resp); - MyUserManager.createNewMyUser( accountName, resp.response.Username, @@ -288,8 +467,27 @@ export function SignUpStepAccountName() { console.log('catch', err); }); }) - .catch(error => { - console.log('error', error); + .catch(reason => { + console.log('error', reason); + + setIsLoading(false); + + let text = + store.getState().appVariables.lang.registration + .signUpStepAccountName.signUpError[ + reason.status as number + ]; + + console.log('text', text, reason.status); + + showToast(toast, { + title: 'Failed', + variant: 'solid', + action: 'error', + description: text, + isClosable: true, + //rest: {colorScheme: 'primary'}, + }); }); }} /> diff --git a/src/user/UserManager.ts b/src/user/UserManager.ts index 6b8404c..8d3c576 100644 --- a/src/user/UserManager.ts +++ b/src/user/UserManager.ts @@ -65,7 +65,8 @@ async function getUser( usr[usrDBKeys.ProfilePictureBinaryLQ].byteLength !== 0 ? createUserProp( SourceProp.offline, - new Blob([usr[usrDBKeys.ProfilePictureBinaryLQ]]), + Buffer.from(usr[usrDBKeys.ProfilePictureBinaryLQ]), + //new Blob([usr[usrDBKeys.ProfilePictureBinaryLQ]]), ) : createUserProp( SourceProp.online, @@ -76,7 +77,8 @@ async function getUser( usr[usrDBKeys.ProfilePictureBinaryHQ].byteLength !== 0 ? createUserProp( SourceProp.offline, - new Blob([usr[usrDBKeys.ProfilePictureBinaryHQ]]), + Buffer.from(usr[usrDBKeys.ProfilePictureBinaryHQ]), + //new Blob([usr[usrDBKeys.ProfilePictureBinaryHQ]]), ) : createUserProp( SourceProp.online, diff --git a/tsconfig.json b/tsconfig.json index d687fbd..faca364 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,11 @@ // ... other configs, if any "baseUrl": ".", "target": "ESNext", + "allowSyntheticDefaultImports": true, + "allowJs": true, + "moduleResolution": "node", + "jsx": "react-native", + "strict": true, "paths": { "@redux/*": ["src/redux/*"], "@lang/*": ["src/lang/*"], @@ -18,7 +23,8 @@ "@navigation/*": ["src/navigation/*"], "@configs/*": ["src/configs/*"], "@helper/*": ["src/helper/*"], - "@user/*": ["src/user/*"] + "@user/*": ["src/user/*"], + "@event/*": ["src/event/*"] } } }