diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..a94f9161 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +npm-debug.log +yarn-error.log +.git +.gitignore +.dockerignore +Dockerfile +*.md +.vscode +.idea +.env +.env.* +build \ No newline at end of file diff --git a/.gitignore b/.gitignore index 25e5f3e2..2a361b5e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ node_modules vite.config.js.timestamp-* vite.config.ts.timestamp-* .idea +.vscode /.npm-only-allow \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index a8a3abc7..d0e76f4f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,7 +6,8 @@ node_modules/ .env .env.* !.env.example - +**/*.lockb +**/*ignore # Ignore files for PNPM, NPM and YARN pnpm-lock.yaml package-lock.json diff --git a/.sonarlint/connectedMode.json b/.sonarlint/connectedMode.json new file mode 100644 index 00000000..e40e90cb --- /dev/null +++ b/.sonarlint/connectedMode.json @@ -0,0 +1,4 @@ +{ + "sonarCloudOrganization": "dval-in", + "projectKey": "dval-in_dvalin-frontend" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..5d5cc0ab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# sonarqube analysis should ignore this file +# sonarqube.analysis.skip=true +FROM node:20-alpine + +# Install pnpm +RUN npm install -g pnpm + +WORKDIR /app + +COPY . . + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy the rest of the application code +COPY . . + + +ARG VITE_BACKEND_URL +ENV VITE_BACKEND_URL=$VITE_BACKEND_URL + +# Build the application +RUN pnpm run build + +# Expose the port the app runs on +EXPOSE 8080 + +# Command to run the application +CMD ["pnpm", "run", "preview"] \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 00000000..93206f36 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index 1882836b..066f6df0 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "layerchart": "^0.43.7", "lucide-svelte": "^0.408.0", "mode-watcher": "^0.4.0", + "simple-icons": "^13.1.0", "socket.io": "^4.7.5", "socket.io-client": "^4.7.5", "svelte-i18next": "^2.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 448a2745..124985de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: mode-watcher: specifier: ^0.4.0 version: 0.4.0(svelte@4.2.18) + simple-icons: + specifier: ^13.1.0 + version: 13.1.0 socket.io: specifier: ^4.7.5 version: 4.7.5 @@ -3448,6 +3451,10 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-icons@13.1.0: + resolution: {integrity: sha512-UUQ5ThpD4J9qkkwnLfloj+mIbJXRHJnMS24lcW+j9ROLpKXysJt2p42iJz/tRi3gEckYaR5+LA5Kb7CZ8Wq8rw==} + engines: {node: '>=0.12.18'} + simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -7659,6 +7666,8 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + simple-icons@13.1.0: {} + simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 diff --git a/src/lib/components/navigator/Sidebar.svelte b/src/lib/components/navigator/Sidebar.svelte index b2f579c1..639f49a9 100644 --- a/src/lib/components/navigator/Sidebar.svelte +++ b/src/lib/components/navigator/Sidebar.svelte @@ -158,7 +158,9 @@ icon={mdiLogout} {isSidebarOpen} link="" - on:click={() => backend.auth.logout()} + on:click={() => { + backend.auth.logout(); + }} title={$i18n.t('navigation.logout')} /> {:else} diff --git a/src/lib/components/navigator/language-switcher/LanguageSwitcher.svelte b/src/lib/components/navigator/language-switcher/LanguageSwitcher.svelte index 75747e1b..891e3de7 100644 --- a/src/lib/components/navigator/language-switcher/LanguageSwitcher.svelte +++ b/src/lib/components/navigator/language-switcher/LanguageSwitcher.svelte @@ -25,9 +25,11 @@ import { AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js'; import Icon from '$lib/components/ui/icon/icon.svelte'; import { mdiTranslate } from '@mdi/js'; + import BackendService from '$lib/services/backend'; let selectedLanguage = get(i18n).language; - + const backend = BackendService.getInstance(); + $: createConfigMutation = backend.user.updateConfig(); let isDialogOpen = false; const languages = Object.keys(get(i18n).options.resources ?? {}); @@ -68,6 +70,9 @@ const saveLanguage = () => { $i18n.changeLanguage(selectedLanguage); isDialogOpen = false; + $createConfigMutation.mutate({ + config: { preferedLanguage: selectedLanguage.toLowerCase() } + }); }; const openDialog = () => { diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/CardContent.svelte similarity index 100% rename from src/lib/components/ui/card/card-content.svelte rename to src/lib/components/ui/card/CardContent.svelte diff --git a/src/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/CardDescription.svelte similarity index 100% rename from src/lib/components/ui/card/card-description.svelte rename to src/lib/components/ui/card/CardDescription.svelte diff --git a/src/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/CardFooter.svelte similarity index 100% rename from src/lib/components/ui/card/card-footer.svelte rename to src/lib/components/ui/card/CardFooter.svelte diff --git a/src/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/CardHeader.svelte similarity index 100% rename from src/lib/components/ui/card/card-header.svelte rename to src/lib/components/ui/card/CardHeader.svelte diff --git a/src/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/CardTitle.svelte similarity index 100% rename from src/lib/components/ui/card/card-title.svelte rename to src/lib/components/ui/card/CardTitle.svelte diff --git a/src/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts index 86c54082..c7800bac 100644 --- a/src/lib/components/ui/card/index.ts +++ b/src/lib/components/ui/card/index.ts @@ -1,9 +1,9 @@ import Root from './card.svelte'; -import Content from './card-content.svelte'; -import Description from './card-description.svelte'; -import Footer from './card-footer.svelte'; -import Header from './card-header.svelte'; -import Title from './card-title.svelte'; +import Content from './CardContent.svelte'; +import Description from './CardDescription.svelte'; +import Footer from './CardFooter.svelte'; +import Header from './CardHeader.svelte'; +import Title from './CardTitle.svelte'; export { Root, diff --git a/src/lib/components/ui/icon-button/IconButton.svelte b/src/lib/components/ui/icon-button/IconButton.svelte index 9dd7a673..53492e87 100644 --- a/src/lib/components/ui/icon-button/IconButton.svelte +++ b/src/lib/components/ui/icon-button/IconButton.svelte @@ -1,8 +1,9 @@ diff --git a/src/lib/components/user/NewAccountForm.svelte b/src/lib/components/user/NewAccountForm.svelte new file mode 100644 index 00000000..fee25e6e --- /dev/null +++ b/src/lib/components/user/NewAccountForm.svelte @@ -0,0 +1,137 @@ + + +
+ {#if Object.keys(errors).length > 0} + + + Error + + Please correct the errors in the form before submitting. + + + {/if} +
+ + + {#if errors.uid} +

{errors.uid}

+ {/if} +
+

Add configuration information (optional)

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+ {#each languages as lang} + + {/each} +
+
+ +
+ +
+
diff --git a/src/lib/locales/EN.json b/src/lib/locales/EN.json index 8b414423..4e545c10 100644 --- a/src/lib/locales/EN.json +++ b/src/lib/locales/EN.json @@ -63,11 +63,28 @@ "characters.detailed.category.builds.artifacts.main_stats.title": "Main Stats", "characters.detailed.category.builds.artifacts.sub_stats.title": "Sub stats", "characters.detailed.category.builds.talents.title": "Talents", + "welcome.title": "Welcome, Traveler!", + "profile.complete.title": "Complete Your Profile to Unlock Full Features", + "profile.complete.description": "By completing your profile, you'll gain access to enhanced features and personalized experiences. Once logged in, you'll be able to import data from various sources and export your information securely.", + "profile.create.success": "Profile created successfully!", + "profile.create.error": "Error creating profile: {error}", + "profile.create.pending.title": "Creating Your Profile", + "profile.create.pending.description": "Please wait while we set up your personalized experience...", + "profile.create.error.title": "Profile Creation Error", "settings.overview.title": "Settings", "settings.category.theming.title": "Theming", "settings.category.data.title": "Data", "settings.category.data.import_data_button": "Import data", "settings.category.data.export_data_button": "Export data", + "settings.login_providers": "Login providers", + "settings.auto_refine_settings": "Auto Refine Settings", + "settings.auto_refine_3_star": "Auto Refine 3-star weapons", + "settings.auto_refine_4_star": "Auto Refine 4-star weapons", + "settings.auto_refine_5_star": "Auto Refine 5-star weapons", + "settings.save_settings": "Save Settings", + "settings.delete_account": "Delete account", + "settings.delete_account_confirmation": "Are you sure you want to delete your account? This action is irreversible and all data will be lost.", + "settings.delete_account_confirm": "Delete account", "settings.import.title": "Import account data", "settings.import.no_file_selected": "No file selected", "settings.import.warning.title": "Watch out!", @@ -76,6 +93,8 @@ "settings.import.confirmation_dialog.title": "Are you sure?", "settings.import.confirmation_dialog.description": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Vitae deserunt velit autem veniam doloribus nulla. Culpa fugiat nemo accusantium cumque soluta tenetur tempora veritatis magnam nulla aliquid. Similique, perspiciatis unde?", "login.title": "Be able to do more with an account!", + "login.alert.title": "Important: Local Data Notice", + "login.alert.description": "Logging in will overwrite your local data. Please ensure you've backed up any important information before proceeding.", "login.perk.cross_device_sync.title": "Cross-device synchronization", "login.perk.cross_device_sync.description": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Vitae deserunt velit autem veniam doloribus nulla. Culpa fugiat nemo accusantium cumque soluta tenetur tempora veritatis magnam nulla aliquid. Similique, perspiciatis unde?", "login.perk.server_access.title": "Access to server side functions", @@ -98,6 +117,14 @@ "server.wish_history.active": "Your wish history is now being processed", "server.wish_history.success": "Your wish history was successfully imported", "server.wish_history.error": "Your wish history failed to import", + "action.reset_filters": "Reset Filters", + "filter.element": "Element", + "filter.weapon": "Weapon", + "filter.rarity": "Rarity", + "filter.owned": "Owned", + "sort.name": "Name", + "sort.date": "Date", + "sort.constellation": "Constellation", "language.DE": "Deutsch", "language.EN": "English", "language.ES": "Español", diff --git a/src/lib/services/backend/auth.ts b/src/lib/services/backend/auth.ts index b829f032..1b3722dd 100644 --- a/src/lib/services/backend/auth.ts +++ b/src/lib/services/backend/auth.ts @@ -1,4 +1,5 @@ import { applicationState } from '$lib/store/application_state'; +import { userProfile, defaultValues } from '$lib/store/user_profile'; export class BackendAuthService { private readonly baseUrl: string; @@ -16,6 +17,9 @@ export class BackendAuthService { state.isAuthenticated = false; return state; }); + userProfile.update(() => { + return defaultValues; + }); window.location.href = `${this.baseUrl}/logout`; } diff --git a/src/lib/services/backend/hoyo.ts b/src/lib/services/backend/hoyo.ts index d38f83aa..c7cb8bb2 100644 --- a/src/lib/services/backend/hoyo.ts +++ b/src/lib/services/backend/hoyo.ts @@ -36,7 +36,7 @@ export class BackendHoyoService { { mutationFn: (authkey: string) => backendFetch( - `${this.baseUrl}/wishhistory?authkey=${authkey}` + `${this.baseUrl}/wish?authkey=${authkey}` ) }, this.queryClient @@ -46,7 +46,7 @@ export class BackendHoyoService { fetchHoyoWishHistoryStatus() { return createQuery( derived(applicationState, (appState) => ({ - queryKey: ['fetchHoyoWishhistoryStatus', appState.isAuthenticated], + queryKey: ['fetchHoyoWishStatus', appState.isAuthenticated], staleTime: 60 * 60 * 1000, //1h queryFn: async () => await backendFetch( @@ -59,6 +59,6 @@ export class BackendHoyoService { } getHoyoWishHistoryStatusUrl() { - return `${this.baseUrl}/wishhistory/status`; + return `${this.baseUrl}/wish/status`; } } diff --git a/src/lib/services/backend/index.ts b/src/lib/services/backend/index.ts index e117b17a..d8e1d80a 100644 --- a/src/lib/services/backend/index.ts +++ b/src/lib/services/backend/index.ts @@ -48,6 +48,24 @@ export const backendFetch = async (url: string): Promise => }).then(checkBackendResponse); }; +export const backendPost = async (url: string, body: object): Promise => { + return fetch(url, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }).then(checkBackendResponse); +}; + +export const backendDelete = async (url: string): Promise => { + return fetch(url, { + method: 'DELETE', + credentials: 'include' + }).then(checkBackendResponse); +}; + export default class BackendService { private env: EnvironmentService = EnvironmentService.getInstance(); private static instance: BackendService | undefined; diff --git a/src/lib/services/backend/user.ts b/src/lib/services/backend/user.ts index 5698693d..b28987ec 100644 --- a/src/lib/services/backend/user.ts +++ b/src/lib/services/backend/user.ts @@ -1,5 +1,5 @@ -import { createQuery, type QueryClient } from '@tanstack/svelte-query'; -import { backendFetch } from '$lib/services/backend/index'; +import { createMutation, createQuery, type QueryClient } from '@tanstack/svelte-query'; +import { backendDelete, backendFetch, backendPost } from '$lib/services/backend/index'; import type { UserProfile } from '$lib/types/user_profile'; import { applicationState } from '$lib/store/application_state'; import { derived } from 'svelte/store'; @@ -27,4 +27,46 @@ export class BackendUserService { this.queryClient ); } + + createUserProfile() { + return createMutation({ + mutationFn: async (data: { uid: number; config: object }) => + await backendPost(`${this.baseUrl}/create`, data) + }); + } + + updateConfig() { + return createMutation({ + mutationFn: async (data: { config: object }) => + await backendPost(`${this.baseUrl}/config`, data) + }); + } + + syncUserProfile() { + return createMutation({ + mutationFn: async (data: { file: object; format: 'paimon' | 'dvalin' }) => + await backendPost(`${this.baseUrl}/sync`, { + ...data.file, + format: data.format + }) + }); + } + + deleteUserProfile() { + return createMutation({ + mutationFn: async () => { + const response = await backendDelete<{ state: 'SUCCESS' }>(`${this.baseUrl}`); + if (response.state === 'SUCCESS') { + this.queryClient.invalidateQueries({ queryKey: ['fetchUserProfile'] }); + return response; + } + throw new Error('Failed to delete user profile'); + }, + onSuccess: () => { + // Optionally, you can perform additional actions on successful deletion + // For example, updating the application state + applicationState.update((state) => ({ ...state, isAuthenticated: false })); + } + }); + } } diff --git a/src/lib/services/importer/dvalin.ts b/src/lib/services/importer/dvalin.ts deleted file mode 100644 index 93a77ea9..00000000 --- a/src/lib/services/importer/dvalin.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { IImporterService } from '$lib/services/importer/index'; -import { isDvalinUserProfile, type UserProfile } from '$lib/types/user_profile'; - -export class DvalinImporterService implements IImporterService { - import(data: unknown): UserProfile { - if (isDvalinUserProfile(data)) { - return { - ...data - }; - } - throw new Error('Make sure you upload the right file format'); - } -} diff --git a/src/lib/services/importer/index.ts b/src/lib/services/importer/index.ts deleted file mode 100644 index e0597acc..00000000 --- a/src/lib/services/importer/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PaimonMoeImporterService } from '$lib/services/importer/paimon'; -import { DvalinImporterService } from '$lib/services/importer/dvalin'; -import type { UserProfile } from '$lib/types/user_profile'; - -const importerServices: { [key: string]: IImporterService } = { - dvalin: new DvalinImporterService(), - paimon: new PaimonMoeImporterService() -}; - -export type ImporterServices = keyof typeof importerServices; - -export interface IImporterService { - import(data: unknown): UserProfile; -} - -export default class ImporterService { - getImporterService(key: ImporterServices) { - return importerServices[key]; - } -} diff --git a/src/lib/services/importer/paimon.ts b/src/lib/services/importer/paimon.ts deleted file mode 100644 index 55d7ef0c..00000000 --- a/src/lib/services/importer/paimon.ts +++ /dev/null @@ -1,664 +0,0 @@ -import { isPaimonData, type PaimonCharacters, type PaimonPulls } from '$lib/types/import/paimon'; -import { isServerKey } from '$lib/types/keys/ServerKey'; -import type { IWish } from '$lib/types/wish'; -import type { WeaponKey } from '$lib/types/keys/WeaponKey'; -import type { CharacterKey } from '$lib/types/keys/CharacterKey'; -import type { ICharacters } from '$lib/types/character'; -import type { BannerKey } from '$lib/types/keys/BannerKey'; -import type { IImporterService } from '$lib/services/importer/index'; -import type { UserProfile } from '$lib/types/user_profile'; - -function convertPaimonCharacter(paimonPullId: string): CharacterKey { - switch (paimonPullId) { - case 'albedo': - return 'Albedo'; - case 'alhaitham': - return 'Alhaitham'; - case 'aloy': - return 'Aloy'; - case 'amber': - return 'Amber'; - case 'arataki_itto': - return 'AratakiItto'; - case 'baizhu': - return 'Baizhu'; - case 'barbara': - return 'Barbara'; - case 'beidou': - return 'Beidou'; - case 'bennett': - return 'Bennett'; - case 'candace': - return 'Candace'; - case 'charlotte': - return 'Charlotte'; - case 'chevreuse': - return 'Chevreuse'; - case 'chongyun': - return 'Chongyun'; - case 'chiori': - return 'Chiori'; - case 'collei': - return 'Collei'; - case 'cyno': - return 'Cyno'; - case 'dehya': - return 'Dehya'; - case 'diluc': - return 'Diluc'; - case 'diona': - return 'Diona'; - case 'dori': - return 'Dori'; - case 'eula': - return 'Eula'; - case 'faruzan': - return 'Faruzan'; - case 'fischl': - return 'Fischl'; - case 'freminet': - return 'Freminet'; - case 'furina': - return 'Furina'; - case 'gaming': - return 'Gaming'; - case 'ganyu': - return 'Ganyu'; - case 'gorou': - return 'Gorou'; - case 'hu_tao': - return 'HuTao'; - case 'jean': - return 'Jean'; - case 'kaedehara_kazuha': - return 'KaedeharaKazuha'; - case 'kaeya': - return 'Kaeya'; - case 'kamisato_ayaka': - return 'KamisatoAyaka'; - case 'kamisato_ayato': - return 'KamisatoAyato'; - case 'kaveh': - return 'Kaveh'; - case 'keqing': - return 'Keqing'; - case 'kirara': - return 'Kirara'; - case 'klee': - return 'Klee'; - case 'kujou_sara': - return 'KujouSara'; - case 'kuki_shinobu': - return 'KukiShinobu'; - case 'layla': - return 'Layla'; - case 'lisa': - return 'Lisa'; - case 'lynette': - return 'Lynette'; - case 'lyney': - return 'Lyney'; - case 'mika': - return 'Mika'; - case 'mona': - return 'Mona'; - case 'nahida': - return 'Nahida'; - case 'navia': - return 'Navia'; - case 'neuvillette': - return 'Neuvillette'; - case 'nilou': - return 'Nilou'; - case 'ningguang': - return 'Ningguang'; - case 'noelle': - return 'Noelle'; - case 'qiqi': - return 'Qiqi'; - case 'raiden_shogun': - return 'RaidenShogun'; - case 'razor': - return 'Razor'; - case 'rosaria': - return 'Rosaria'; - case 'sangonomiya_kokomi': - return 'SangonomiyaKokomi'; - case 'sayu': - return 'Sayu'; - case 'shenhe': - return 'Shenhe'; - case 'shikanoin_heizou': - return 'ShikanoinHeizou'; - case 'somnia': - return 'Somnia'; - case 'sucrose': - return 'Sucrose'; - case 'tartaglia': - return 'Tartaglia'; - case 'thoma': - return 'Thoma'; - case 'tighnari': - return 'Tighnari'; - case 'traveler_geo': - case 'traveler_anemo': - case 'traveler_electro': - case 'traveler_dendro': - case 'traveler_hydro': - return 'Traveler'; - case 'venti': - return 'Venti'; - case 'wanderer': - return 'Wanderer'; - case 'wriothesley': - return 'Wriothesley'; - case 'xiangling': - return 'Xiangling'; - case 'xianyun': - return 'Xianyun'; - case 'xiao': - return 'Xiao'; - case 'xingqiu': - return 'Xingqiu'; - case 'xinyan': - return 'Xinyan'; - case 'yae_miko': - return 'YaeMiko'; - case 'yanfei': - return 'Yanfei'; - case 'yaoyao': - return 'Yaoyao'; - case 'yelan': - return 'Yelan'; - case 'yoimiya': - return 'Yoimiya'; - case 'yun_jin': - return 'YunJin'; - case 'zhongli': - return 'Zhongli'; - default: - throw new Error('Unknown key ' + paimonPullId); - } -} - -function convertPaimonWeapon(paimonPullId: string): WeaponKey { - switch (paimonPullId) { - case 'unknown_3_star': - return 'Unknown3Star'; - case 'a_thousand_floating_dreams': - return 'AThousandFloatingDreams'; - case 'akuoumaru': - return 'Akuoumaru'; - case 'alley_hunter': - return 'AlleyHunter'; - case 'amenoma_kageuchi': - return 'AmenomaKageuchi'; - case 'amos_bow': - return 'AmosBow'; - case 'apprentices_notes': - return 'ApprenticesNotes'; - case 'aqua_simulacra': - return 'AquaSimulacra'; - case 'aquila_favonia': - return 'AquilaFavonia'; - case 'ballad_of_the_boundless_blue': - return 'BalladOfTheBoundlessBlue'; - case 'ballad_of_the_fjords': - return 'BalladOfTheFjords'; - case 'beacon_of_the_reed_sea': - return 'BeaconOfTheReedSea'; - case 'beginners_protector': - return 'BeginnersProtector'; - case 'black_tassel': - return 'BlackTassel'; - case 'blackcliff_agate': - return 'BlackcliffAgate'; - case 'blackcliff_longsword': - return 'BlackcliffLongsword'; - case 'blackcliff_pole': - return 'BlackcliffPole'; - case 'blackcliff_slasher': - return 'BlackcliffSlasher'; - case 'blackcliff_warbow': - return 'BlackcliffWarbow'; - case 'bloodtainted_greatsword': - return 'BloodtaintedGreatsword'; - case 'calamity_queller': - return 'CalamityQueller'; - case 'cashflow_supervision': - return 'CashflowSupervision'; - case 'cinnabar_spindle': - return 'CinnabarSpindle'; - case 'compound_bow': - return 'CompoundBow'; - case 'cool_steel': - return 'CoolSteel'; - case 'cranes_echoing_call': - return 'CranesEchoingCall'; - case 'crescent_pike': - return 'CrescentPike'; - case 'dark_iron_sword': - return 'DarkIronSword'; - case 'deathmatch': - return 'Deathmatch'; - case 'debate_club': - return 'DebateClub'; - case 'dialogues_of_the_desert_sages': - return 'DialoguesOfTheDesertSages'; - case 'dodoco_tales': - return 'DodocoTales'; - case 'dragons_bane': - return 'DragonsBane'; - case 'dragonspine_spear': - return 'DragonspineSpear'; - case 'dull_blade': - return 'DullBlade'; - case 'elegy_for_the_end': - return 'ElegyForTheEnd'; - case 'emerald_orb': - return 'EmeraldOrb'; - case 'end_of_the_line': - return 'EndOfTheLine'; - case 'engulfing_lightning': - return 'EngulfingLightning'; - case 'everlasting_moonglow': - return 'EverlastingMoonglow'; - case 'eye_of_perception': - return 'EyeOfPerception'; - case 'fading_twilight': - return 'FadingTwilight'; - case 'favonius_codex': - return 'FavoniusCodex'; - case 'favonius_greatsword': - return 'FavoniusGreatsword'; - case 'favonius_lance': - return 'FavoniusLance'; - case 'favonius_sword': - return 'FavoniusSword'; - case 'favonius_warbow': - return 'FavoniusWarbow'; - case 'ferrous_shadow': - return 'FerrousShadow'; - case 'festering_desire': - return 'FesteringDesire'; - case 'fillet_blade': - return 'FilletBlade'; - case 'finale_of_the_deep': - return 'FinaleOfTheDeep'; - case 'fleuve_cendre_ferryman': - return 'FleuveCendreFerryman'; - case 'flowing_purity': - return 'FlowingPurity'; - case 'forest_regalia': - return 'ForestRegalia'; - case 'freedom-sworn': - return 'FreedomSworn'; - case 'frostbearer': - return 'Frostbearer'; - case 'fruit_of_fulfillment': - return 'FruitOfFulfillment'; - case 'hakushin_ring': - return 'HakushinRing'; - case 'halberd': - return 'Halberd'; - case 'hamayumi': - return 'Hamayumi'; - case 'haran_geppaku_futsu': - return 'HaranGeppakuFutsu'; - case 'harbinger_of_dawn': - return 'HarbingerOfDawn'; - case 'hunters_bow': - return 'HuntersBow'; - case 'hunters_path': - return 'HuntersPath'; - case 'ibis_piercer': - return 'IbisPiercer'; - case 'iron_point': - return 'IronPoint'; - case 'iron_sting': - return 'IronSting'; - case 'jadefalls_splendor': - return 'JadefallsSplendor'; - case 'kagotsurube_isshin': - return 'KagotsurubeIsshin'; - case 'kaguras_verity': - return 'KagurasVerity'; - case 'katsuragikiri_nagamasa': - return 'KatsuragikiriNagamasa'; - case 'key_of_khaj-nisut': - return 'KeyOfKhajNisut'; - case 'kings_squire': - return 'KingsSquire'; - case 'kitain_cross_spear': - return 'KitainCrossSpear'; - case 'light_of_foliar_incision': - return 'LightOfFoliarIncision'; - case 'lions_roar': - return 'LionsRoar'; - case 'lithic_blade': - return 'LithicBlade'; - case 'lithic_spear': - return 'LithicSpear'; - case 'lost_prayer_to_the_sacred_winds': - return 'LostPrayerToTheSacredWinds'; - case 'luxurious_sea_lord': - return 'LuxuriousSeaLord'; - case 'magic_guide': - return 'MagicGuide'; - case 'mailed_flower': - return 'MailedFlower'; - case 'makhaira_aquamarine': - return 'MakhairaAquamarine'; - case 'mappa_mare': - return 'MappaMare'; - case 'memory_of_dust': - return 'MemoryOfDust'; - case 'messenger': - return 'Messenger'; - case 'missive_windspear': - return 'MissiveWindspear'; - case 'mistsplitter_reforged': - return 'MistsplitterReforged'; - case 'mitternachts_waltz': - return 'MitternachtsWaltz'; - case 'moonpiercer': - return 'Moonpiercer'; - case 'mouuns_moon': - return 'MouunsMoon'; - case 'oathsworn_eye': - return 'OathswornEye'; - case 'old_mercs_pal': - return 'OldMercsPal'; - case 'otherworldly_story': - return 'OtherworldlyStory'; - case 'pocket_grimoire': - return 'PocketGrimoire'; - case 'polar_star': - return 'PolarStar'; - case 'portable_power_saw': - return 'PortablePowerSaw'; - case 'predator': - return 'Predator'; - case 'primordial_jade_cutter': - return 'PrimordialJadeCutter'; - case 'primordial_jade_winged-spear': - return 'PrimordialJadeWingedSpear'; - case 'prospectors_drill': - return 'ProspectorsDrill'; - case 'prototype_amber': - return 'PrototypeAmber'; - case 'prototype_archaic': - return 'PrototypeArchaic'; - case 'prototype_crescent': - return 'PrototypeCrescent'; - case 'prototype_rancour': - return 'PrototypeRancour'; - case 'prototype_starglitter': - return 'PrototypeStarglitter'; - case 'quantum_catalyst': - return 'QuantumCatalyst'; - case 'rainslasher': - return 'Rainslasher'; - case 'range_gauge': - return 'RangeGauge'; - case 'raven_bow': - return 'RavenBow'; - case 'recurve_bow': - return 'RecurveBow'; - case 'redhorn_stonethresher': - return 'RedhornStonethresher'; - case 'rightful_reward': - return 'RightfulReward'; - case 'royal_bow': - return 'RoyalBow'; - case 'royal_greatsword': - return 'RoyalGreatsword'; - case 'royal_grimoire': - return 'RoyalGrimoire'; - case 'royal_longsword': - return 'RoyalLongsword'; - case 'royal_spear': - return 'RoyalSpear'; - case 'rust': - return 'Rust'; - case 'sacrificial_bow': - return 'SacrificialBow'; - case 'sacrificial_fragments': - return 'SacrificialFragments'; - case 'sacrificial_greatsword': - return 'SacrificialGreatsword'; - case 'sacrificial_jade': - return 'SacrificialJade'; - case 'sacrificial_sword': - return 'SacrificialSword'; - case 'sapwood_blade': - return 'SapwoodBlade'; - case 'scion_of_the_blazing_sun': - return 'ScionOfTheBlazingSun'; - case 'seasoned_hunters_bow': - return 'SeasonedHuntersBow'; - case 'serpent_spine': - return 'SerpentSpine'; - case 'sharpshooters_oath': - return 'SharpshootersOath'; - case 'silver_sword': - return 'SilverSword'; - case 'skyrider_greatsword': - return 'SkyriderGreatsword'; - case 'skyrider_sword': - return 'SkyriderSword'; - case 'skyward_atlas': - return 'SkywardAtlas'; - case 'skyward_blade': - return 'SkywardBlade'; - case 'skyward_harp': - return 'SkywardHarp'; - case 'skyward_pride': - return 'SkywardPride'; - case 'skyward_spine': - return 'SkywardSpine'; - case 'slingshot': - return 'Slingshot'; - case 'snow_tombed_starsilver': - return 'SnowTombedStarsilver'; - case 'solar_pearl': - return 'SolarPearl'; - case 'song_of_broken_pines': - return 'SongOfBrokenPines'; - case 'song_of_stillness': - return 'SongOfStillness'; - case 'splendor_of_tranquil_waters': - return 'SplendorOfTranquilWaters'; - case 'staff_of_homa': - return 'StaffOfHoma'; - case 'staff_of_the_scarlet_sands': - return 'StaffOfTheScarletSands'; - case 'summit_shaper': - return 'SummitShaper'; - case 'sword_of_descension': - return 'SwordOfDescension'; - case 'sword_of_narzissenkreuz': - return 'SwordOfNarzissenkreuz'; - case 'talking_stick': - return 'TalkingStick'; - case 'the_alley_flash': - return 'TheAlleyFlash'; - case 'the_bell': - return 'TheBell'; - case 'the_black_sword': - return 'TheBlackSword'; - case 'the_catch': - return 'TheCatch'; - case 'the_dockhands_assistant': - return 'TheDockhandsAssistant'; - case 'the_first_great_magic': - return 'TheFirstGreatMagic'; - case 'the_flute': - return 'TheFlute'; - case 'the_stringless': - return 'TheStringless'; - case 'the_unforged': - return 'TheUnforged'; - case 'the_viridescent_hunt': - return 'TheViridescentHunt'; - case 'the_widsith': - return 'TheWidsith'; - case 'thrilling_tales_of_dragon_slayers': - return 'ThrillingTalesOfDragonSlayers'; - case 'thundering_pulse': - return 'ThunderingPulse'; - case 'tidal_shadow': - return 'TidalShadow'; - case 'tome_of_the_eternal_flow': - return 'TomeOfTheEternalFlow'; - case 'toukabou_shigure': - return 'ToukabouShigure'; - case 'travelers_handy_sword': - return 'TravelersHandySword'; - case 'tulaytullahs_remembrance': - return 'TulaytullahsRemembrance'; - case 'twin_nephrite': - return 'TwinNephrite'; - case 'ultimate_overlords_mega_magic_sword': - return 'UltimateOverlordsMegaMagicSword'; - case 'uraku_misugiri': - return 'UrakuMisugiri'; - case 'verdict': - return 'Verdict'; - case 'vortex_vanquisher': - return 'VortexVanquisher'; - case 'wandering_evenstar': - return 'WanderingEvenstar'; - case 'waster_greatsword': - return 'WasterGreatsword'; - case 'wavebreakers_fin': - return 'WavebreakersFin'; - case 'white_iron_greatsword': - return 'WhiteIronGreatsword'; - case 'white_tassel': - return 'WhiteTassel'; - case 'whiteblind': - return 'Whiteblind'; - case 'windblume_ode': - return 'WindblumeOde'; - case 'wine_and_song': - return 'WineAndSong'; - case 'wolf_fang': - return 'WolfFang'; - case 'wolfs_gravestone': - return 'WolfsGravestone'; - case 'xiphos_moonlight': - return 'XiphosMoonlight'; - default: - throw new Error('Unknown key ' + paimonPullId); - } -} - -function convertPaimonId( - paimonPullId: string, - paimonPullType: 'character' | 'weapon' -): CharacterKey | WeaponKey { - return paimonPullType === 'character' - ? convertPaimonCharacter(paimonPullId) - : convertPaimonWeapon(paimonPullId); -} - -function convertPaimonType(paimonPullType: 'character' | 'weapon'): 'Character' | 'Weapon' { - switch (paimonPullType) { - case 'character': - return 'Character'; - case 'weapon': - return 'Weapon'; - } -} - -function convertWishDateToBanner(): BannerKey { - return 'BalladInGoblets1'; -} - -function convertPaimonWishes(paimonWish: PaimonPulls[]): IWish[] { - return paimonWish.toReversed().flatMap((pull, count) => { - return { - type: convertPaimonType(pull.type), - number: paimonWish.length - count, - key: convertPaimonId(pull.id, pull.type), - date: new Date(pull.time), - pity: pull.pity, - banner: convertWishDateToBanner() - }; - }); -} - -function convertPaimonCharacters(paimonCharacters: PaimonCharacters): ICharacters { - const convertedCharacters: ICharacters = {}; - - Object.keys(paimonCharacters).forEach((key) => { - const convertedKey = convertPaimonCharacter(key); - - convertedCharacters[convertedKey] = { - level: 0, - constellation: 0, - ascension: 0, - talent: { - auto: 0, - skill: 0, - burst: 0 - } - }; - }); - - return convertedCharacters; -} - -export class PaimonMoeImporterService implements IImporterService { - import(data: unknown): UserProfile { - if (isPaimonData(data)) { - return { - format: 'dvalin', - version: 0, - user: { - ar: data.ar, - ...(data.server !== undefined && isServerKey(data.server) - ? { server: data.server } - : undefined), - ...(data['wish-uid'] !== undefined ? { uid: data['wish-uid'] } : undefined), - wl: data.wl - }, - ...(data.characters !== undefined - ? { characters: convertPaimonCharacters(data.characters) } - : undefined), - wishes: { - ...(data['wish-counter-character-event'] !== undefined - ? { - CharacterEvent: convertPaimonWishes( - data['wish-counter-character-event'].pulls - ) - } - : undefined), - ...(data['wish-counter-weapon-event'] !== undefined - ? { - WeaponEvent: convertPaimonWishes( - data['wish-counter-weapon-event'].pulls - ) - } - : undefined), - ...(data['wish-counter-standard'] !== undefined - ? { Standard: convertPaimonWishes(data['wish-counter-standard'].pulls) } - : undefined), - ...(data['wish-counter-beginners'] !== undefined - ? { Beginner: convertPaimonWishes(data['wish-counter-beginners'].pulls) } - : undefined), - ...(data['wish-counter-chronicled'] !== undefined - ? { - Chronicled: convertPaimonWishes( - data['wish-counter-chronicled'].pulls - ) - } - : undefined) - } - }; - } else { - throw new Error('Make sure you upload the right file format'); - } - } -} diff --git a/src/lib/store/user_profile.ts b/src/lib/store/user_profile.ts index e476ed13..abd4e6b9 100644 --- a/src/lib/store/user_profile.ts +++ b/src/lib/store/user_profile.ts @@ -1,14 +1,24 @@ import { persisted } from 'svelte-persisted-store'; import type { UserProfile } from '$lib/types/user_profile'; -const defaultValues: UserProfile = { +export const defaultValues: UserProfile = { format: 'dvalin', version: 0, - user: { + config: { + autoRefine3: false, + autoRefine4: false, + autoRefine5: false, + preferedLanguage: 'en' + }, + account: { ar: 0, uid: 0, - wl: 0 - } + wl: 0, + name: '', + server: '', + namecard: '' + }, + auth: [] }; export const userProfile = persisted('userProfile', defaultValues, { diff --git a/src/lib/types/achievement.ts b/src/lib/types/achievement.ts index f88d65c9..da6752eb 100644 --- a/src/lib/types/achievement.ts +++ b/src/lib/types/achievement.ts @@ -1,16 +1,3 @@ -import type { AchievementCategoryKey } from '$lib/types/keys/AchievementCategoryKey'; -import type { AchievementKey } from '$lib/types/keys/AchievementKey'; - -export type IAchievementCategory = { - [key in AchievementCategoryKey]: IAchievements; -}; - export type IAchievements = { - [key in AchievementKey]: IAchievement; -}; - -export type IAchievement = { - category: AchievementCategoryKey; - achieved: boolean; - preStage?: AchievementKey; // Refer to the previous achievements of a series + [key in number]: boolean; }; diff --git a/src/lib/types/artifact.ts b/src/lib/types/artifact.ts index 445c5107..1e16eac2 100644 --- a/src/lib/types/artifact.ts +++ b/src/lib/types/artifact.ts @@ -1,7 +1,7 @@ -import type { ArtifactSetKey } from '$lib/types/keys/ArtifactSetKey'; -import type { SlotKey } from '$lib/types/keys/SlotKey'; -import type { StatKey } from '$lib/types/keys/StatKey'; -import type { CharacterKey } from '$lib/types/keys/CharacterKey'; +import type { SlotKey } from './artifact_slot'; +import type { StatKey } from './artifact_stat'; +import type { ArtifactSetKey } from './keys/ArtifactSetKey'; +import type { CharacterKey } from './keys/CharacterKey'; export type IArtifact = { setKey: ArtifactSetKey; // E.g. "GladiatorsFinale" @@ -9,12 +9,12 @@ export type IArtifact = { level: number; // 0-20 inclusive rarity: number; // 1-5 inclusive mainStatKey: StatKey; - location: CharacterKey | ''; // Where "" means not equipped. + characterKey: CharacterKey | ''; // Where "" means not equipped. lock: boolean; // Whether the artifact is locked in game. substats: ISubstat[]; }; export type ISubstat = { - key: StatKey; // E.g. "critDMG_" + key: StatKey; value: number; // E.g. 19.4 }; diff --git a/src/lib/types/artifact_slot.ts b/src/lib/types/artifact_slot.ts new file mode 100644 index 00000000..49eb2d82 --- /dev/null +++ b/src/lib/types/artifact_slot.ts @@ -0,0 +1,6 @@ +const slotKeys = ['flower', 'plume', 'sands', 'goblet', 'circlet']; +export type SlotKey = (typeof slotKeys)[number]; + +export const isSlotKey = (key: string): key is SlotKey => { + return slotKeys.includes(key); +}; diff --git a/src/lib/types/artifact_stat.ts b/src/lib/types/artifact_stat.ts new file mode 100644 index 00000000..415badf0 --- /dev/null +++ b/src/lib/types/artifact_stat.ts @@ -0,0 +1,27 @@ +const statKeys = [ + 'hp', + 'hp%', + 'atk', + 'atk%', + 'def', + 'def%', + 'elementalMastery', + 'energyRecharge', + 'healingBonus', + 'critRate', + 'critDMG', + 'physicalDmg', + 'anemoDmg', + 'geoDmg', + 'electroDmg', + 'hydroDmg', + 'pyroDmg', + 'cryoDmg', + 'dendroDmg' +]; + +export type StatKey = (typeof statKeys)[number]; + +export const isStatKey = (key: string): key is StatKey => { + return statKeys.includes(key); +}; diff --git a/src/lib/types/character.ts b/src/lib/types/character.ts index 80030f0e..7e5039eb 100644 --- a/src/lib/types/character.ts +++ b/src/lib/types/character.ts @@ -1,7 +1,7 @@ -import type { CharacterKey } from '$lib/types/keys/CharacterKey'; +import type { CharacterKey } from './keys/CharacterKey'; export type ICharacters = { - [key in CharacterKey]?: ICharacter; + [key in CharacterKey]: ICharacter; // E.g. "Rosaria" }; export type ICharacter = { @@ -9,9 +9,10 @@ export type ICharacter = { constellation: number; // 0-6 inclusive ascension: number; // 0-6 inclusive. need to disambiguate 80/90 or 80/80 talent: { - // Does not include boost from constellations. 1-15 inclusive + // Does not include boost from constellations. auto: number; skill: number; burst: number; }; + manualConstellations: number; // constellation added by the user }; diff --git a/src/lib/types/config.ts b/src/lib/types/config.ts new file mode 100644 index 00000000..7bd4350e --- /dev/null +++ b/src/lib/types/config.ts @@ -0,0 +1,8 @@ +import type { Locale } from './locale'; + +export type Config = { + autoRefine3: boolean; + autoRefine4: boolean; + autoRefine5: boolean; + preferedLanguage: Locale; +}; diff --git a/src/lib/types/server.ts b/src/lib/types/server.ts new file mode 100644 index 00000000..6dd553dc --- /dev/null +++ b/src/lib/types/server.ts @@ -0,0 +1,7 @@ +const serverKeys = ['Europe', 'America', 'Asia', 'HK-TW', 'China']; + +export type ServerKey = (typeof serverKeys)[number]; + +export const isServerKey = (key: string): key is ServerKey => { + return serverKeys.includes(key); +}; diff --git a/src/lib/types/user.ts b/src/lib/types/user.ts index 21eb3c1c..4794264e 100644 --- a/src/lib/types/user.ts +++ b/src/lib/types/user.ts @@ -1,8 +1,11 @@ -import type { ServerKey } from '$lib/types/keys/ServerKey'; +import type { ServerKey } from './server'; export interface IUser { - server?: ServerKey; + server: ServerKey; ar: number; - uid?: number; + uid: number; wl: number; + name: string; + namecard: string; + signature?: string; } diff --git a/src/lib/types/user_profile.ts b/src/lib/types/user_profile.ts index afe87894..41cc10e2 100644 --- a/src/lib/types/user_profile.ts +++ b/src/lib/types/user_profile.ts @@ -1,17 +1,21 @@ import type { IUser } from '$lib/types/user'; import type { ICharacters } from '$lib/types/character'; import type { IArtifact } from '$lib/types/artifact'; -import type { IAchievementCategory } from '$lib/types/achievement'; +import type { IAchievements } from '$lib/types/achievement'; import type { IFurnishings } from '$lib/types/furnishing'; import type { IMaterials } from '$lib/types/material'; import type { IWeapon } from '$lib/types/weapon'; import type { IWishes } from '$lib/types/wish'; +import type { Config } from './config'; export interface UserProfile { format: 'dvalin'; version: number; - user?: IUser; - achievements?: IAchievementCategory; + config: Config; + account: IUser; + auth: string[]; + lastUpdated?: Date; + achievements?: IAchievements; artifacts?: IArtifact[]; characters?: ICharacters; furnishing?: IFurnishings; diff --git a/src/lib/types/weapon.ts b/src/lib/types/weapon.ts index 91b369b5..f57441b8 100644 --- a/src/lib/types/weapon.ts +++ b/src/lib/types/weapon.ts @@ -1,13 +1,13 @@ -import type { WeaponKey } from '$lib/types/keys/WeaponKey'; import type { CharacterKey } from '$lib/types/keys/CharacterKey'; +import type { WeaponKey } from './keys/WeaponKey'; export type WeaponTypes = 'bow' | 'catalyst' | 'claymore' | 'polearm' | 'sword'; export type IWeapon = { - key: WeaponKey; // "CrescentPike" + id: WeaponKey; // "CrescentPike" + key: string; // "CrescentPike" level: number; // 1-90 inclusive ascension: number; // 0-6 inclusive. need to disambiguate 80/90 or 80/80 refinement: number; // 1-5 inclusive - location: CharacterKey | ''; // Where "" means not equipped. - lock: boolean; // Whether the weapon is locked in game. + characterKey?: CharacterKey; }; diff --git a/src/lib/types/wish.ts b/src/lib/types/wish.ts index 8c382dad..d48ee3a8 100644 --- a/src/lib/types/wish.ts +++ b/src/lib/types/wish.ts @@ -18,6 +18,7 @@ export type IWish = { date: Date; pity: number; banner: BannerKey; + rarity: number; }; export type IMappedWish = { diff --git a/src/lib/utils/nav_helper.ts b/src/lib/utils/nav_helper.ts new file mode 100644 index 00000000..878586e4 --- /dev/null +++ b/src/lib/utils/nav_helper.ts @@ -0,0 +1,5 @@ +const nav = (url: string) => { + window.location.href = url; +}; + +export { nav }; diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index de8c1039..8fce2a9f 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -7,9 +7,10 @@ import { get } from 'svelte/store'; import { toast } from 'svelte-sonner'; import { userProfile } from '$lib/store/user_profile'; import i18n from '$lib/services/i18n'; +import { goto } from '$app/navigation'; /** @type {import('../../.svelte-kit/types/src/routes/$types').LayoutServerLoad} */ -export async function load() { +export const load = async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -22,17 +23,24 @@ export async function load() { if (browser) { backend.user.fetchUserProfile().subscribe((response) => { if (response.status === 'success' && response.data.state === 'SUCCESS') { - userProfile.set(response.data.data); + userProfile.update((currentProfile) => ({ + ...currentProfile, + ...response.data.data, + lastUpdated: new Date() + })); } }); const socket = io(import.meta.env.VITE_BACKEND_URL, { withCredentials: true }); socket.on('authenticationState', (authenticationState: boolean) => { - applicationState.update((state) => { - state.isAuthenticated = authenticationState; - return state; - }); + if (authenticationState && get(userProfile).account.uid < 10000) { + goto('/settings/firstlogin'); + } + applicationState.update((state) => ({ + ...state, + isAuthenticated: authenticationState + })); }); socket.on('invalidateQuery', (queryKey: string[]) => { @@ -57,4 +65,4 @@ export async function load() { } return { queryClient, backend }; -} +}; diff --git a/src/routes/characters/+page.svelte b/src/routes/characters/+page.svelte index e5adf005..90e97a33 100644 --- a/src/routes/characters/+page.svelte +++ b/src/routes/characters/+page.svelte @@ -59,21 +59,21 @@ const filterStore = writable<{ filterFn: Filters; value: string }[]>([]); const elements = [ - { name: 'pyro', icon: IconPyro, label: 'Pyro', color: '#ff6640' }, - { name: 'hydro', icon: IconHydro, label: 'Hydro', color: '#00c0ff' }, - { name: 'anemo', icon: IconAnemo, label: 'Anemo', color: '#32d7a0' }, - { name: 'electro', icon: IconElectro, label: 'Electro', color: '#cc80ff' }, - { name: 'dendro', icon: IconDendro, label: 'Dendro', color: '#90cc00' }, - { name: 'cryo', icon: IconCryo, label: 'Cryo', color: '#81fffe' }, - { name: 'geo', icon: IconGeo, label: 'Geo', color: '#ffac00' } + { name: 'pyro', icon: IconPyro, label: $i18n.t('element.pyro'), color: '#ff6640' }, + { name: 'hydro', icon: IconHydro, label: $i18n.t('element.hydro'), color: '#00c0ff' }, + { name: 'anemo', icon: IconAnemo, label: $i18n.t('element.anemo'), color: '#32d7a0' }, + { name: 'electro', icon: IconElectro, label: $i18n.t('element.electro'), color: '#cc80ff' }, + { name: 'dendro', icon: IconDendro, label: $i18n.t('element.dendro'), color: '#90cc00' }, + { name: 'cryo', icon: IconCryo, label: $i18n.t('element.cryo'), color: '#81fffe' }, + { name: 'geo', icon: IconGeo, label: $i18n.t('element.geo'), color: '#ffac00' } ]; const weapons = [ - { name: 'sword', icon: mdiSwordCross, label: 'Sword' }, - { name: 'claymore', icon: mdiSword, label: 'Claymore' }, - { name: 'polearm', icon: mdiSpear, label: 'Polearm' }, - { name: 'catalyst', icon: mdiBookOpenPageVariant, label: 'Catalyst' }, - { name: 'bow', icon: mdiBowArrow, label: 'Bow' } + { name: 'sword', icon: mdiSwordCross, label: $i18n.t('weapon.sword') }, + { name: 'claymore', icon: mdiSword, label: $i18n.t('weapon.claymore') }, + { name: 'polearm', icon: mdiSpear, label: $i18n.t('weapon.polearm') }, + { name: 'catalyst', icon: mdiBookOpenPageVariant, label: $i18n.t('weapon.catalyst') }, + { name: 'bow', icon: mdiBowArrow, label: $i18n.t('weapon.bow') } ]; const checkedStore = writable<{ [key: string]: boolean }>({}); @@ -124,7 +124,6 @@ const obtained = userProfile.characters ? Object.keys(userProfile.characters).includes(key) : false; - return { obtained, link: `/characters/${key}`, @@ -234,7 +233,9 @@ icon={$sortStore.order === 'asc' ? mdiSortAscending : mdiSortDescending} class={`flex w-full`} > - {$i18n.t('action.sort_by', { sortFN: $sortStore.sortFn })} + {$i18n.t('action.sort_by', { + sortFN: $i18n.t(`sort.${$sortStore.sortFn.toLowerCase()}`) + })} @@ -242,13 +243,15 @@ class="flex hover:bg-tertiary gap-2" on:click={() => setSortStore('Name')} > - Name + + {$i18n.t('sort.name')} setSortStore('Date')} > - Date + + {$i18n.t('sort.date')} setSortStore('Constellation')} > - Constellation + + {$i18n.t('sort.constellation')} @@ -307,13 +311,12 @@ disabled={$filterStore.length === 0} on:click={() => resetFilters()} > - Reset Filters + {$i18n.t('action.reset_filters')} - - Element + {$i18n.t('filter.element')} {#each elements as { name, icon, label }} @@ -338,7 +341,7 @@ - Weapon + {$i18n.t('filter.weapon')} {#each weapons as { name, icon }} @@ -363,7 +366,7 @@ - Rarity + {$i18n.t('filter.rarity')} +
Element
-
+
{#each elements as { name, icon, label }} -
{ setFilterStore('element', name); toggleChecked(name); }} + on:keydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + setFilterStore('element', name); + toggleChecked(name); + } + }} > - + {label} -
+ {/each}
+
Weapon
-
+
{#each weapons as { name, icon }} -
{ setFilterStore('weapon', name); toggleChecked(name); }} + on:keydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + setFilterStore('weapon', name); + toggleChecked(name); + } + }} > - + -
+ {/each}
+
Rarity
-
-
+
-
+
+
+
Ownership
-
-
+
-
+
+
diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index 908d70d4..c8268334 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -1,86 +1,81 @@ -
-
-
- -
- {$i18n.t('login.perk.cross_device_sync.title')} - - {$i18n.t('login.perk.cross_device_sync.description')} - -
-
-
- -
- {$i18n.t('login.perk.server_access.title')} - - {$i18n.t('login.perk.server_access.description')} - -
-
-
- -
- {$i18n.t('login.perk.personal_profile.title')} - - {$i18n.t('login.perk.personal_profile.description')} - +
+
+ {#each perks as perk} +
+ +
+

{$i18n.t(perk.title)}

+

{$i18n.t(perk.description)}

+
-
+ {/each}
-
- +
+ - Local data will be overwritten once you log in - Please backup your data before continuing + {$i18n.t('login.alert.title')} + {$i18n.t('login.alert.description')} - nav(backend.auth.login('google'))} - > - Google - - nav(backend.auth.login('microsoft'))} - > - Microsoft - - nav(backend.auth.login('github'))} - > - Github - + {#each loginOptions as option} + + {/each}
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index b46927c6..3896e1f3 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -3,12 +3,47 @@ import Text from '$lib/components/typography/Text.svelte'; import { applicationState } from '$lib/store/application_state'; import IconButton from '$lib/components/ui/icon-button/IconButton.svelte'; - import { mdiExport, mdiImport } from '@mdi/js'; + import { mdiDelete, mdiExport, mdiImport, mdiAlert, mdiMicrosoft } from '@mdi/js'; import type { Theme } from '$lib/types/theme'; import { Button } from '$lib/components/ui/button'; import DefaultLayout from '$lib/components/layout/DefaultLayout.svelte'; import i18n from '$lib/services/i18n'; - import { userProfile } from '$lib/store/user_profile'; + import { defaultValues, userProfile } from '$lib/store/user_profile'; + import BackendService from '$lib/services/backend'; + import { siGoogle, siDiscord, siGithub, type SimpleIcon } from 'simple-icons'; + import Icon from '$lib/components/ui/icon/icon.svelte'; + import Card from '$lib/components/ui/card/card.svelte'; + import { CardHeader, CardTitle, CardContent } from '$lib/components/ui/card'; + import { Checkbox } from '$lib/components/ui/checkbox'; + import { Label } from '$lib/components/ui/label'; + import { goto } from '$app/navigation'; + import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger + } from '$lib/components/ui/dialog'; + import { nav } from '$lib/utils/nav_helper'; + const backend = BackendService.getInstance(); + const createConfigMutation = backend.user.updateConfig(); + const deleteProfileMutation = backend.user.deleteUserProfile(); + + const loginOptions = [ + { name: 'Google', icon: siGoogle, action: () => nav(backend.auth.login('google')) }, + { name: 'Discord', icon: siDiscord, action: () => nav(backend.auth.login('discord')) }, + { + name: 'Microsoft', + icon: mdiMicrosoft, + action: () => nav(backend.auth.login('microsoft')) + }, + { name: 'Github', icon: siGithub, action: () => nav(backend.auth.login('github')) } + ]; + + const isSimpleIcon = (icon: string | SimpleIcon): icon is SimpleIcon => { + return typeof icon === 'object' && 'path' in icon; + }; const handleSettingsExport = () => { let element = document.createElement('a'); @@ -23,7 +58,9 @@ element.click(); document.body.removeChild(element); }; - + const userProfil = $userProfile; + const user = userProfil.account; + const settings = userProfil.config; // Function to change theme to selected function changeThemeTo(themeName: Theme) { applicationState.update((state) => { @@ -36,10 +73,131 @@ }; }); } + + function handleSave() { + $createConfigMutation.mutate({ + config: { ...get(userProfile).config } + }); + } + // TODO : fix this once we have image on s3 + const bgImageUrl = get(userProfile).account.namecard; + + function handleDelete(): void { + $deleteProfileMutation.mutate(); + applicationState.update((state) => { + return { + ...state, + isAuthenticated: false + }; + }); + userProfile.update(() => { + return defaultValues; + }); + goto('/'); + } -
+ {#if $applicationState.isAuthenticated} +
+ +
+
+ + {user.name} +

UID: {user.uid}

+
+ +

+ Server: + {user.server} +

+

+ Adventure Rank: + {user.ar} +

+

+ World Level: + {user.wl} +

+

"{user.signature}"

+
+
+
+
+ + Profiles + +
+ +
+
+
+
+ {$i18n.t('settings.login_providers')} +
+ {#each loginOptions as option} + + {/each} +
+
+
+
+
+ {$i18n.t('settings.auto_refine_settings')} +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ {/if} +
{$i18n.t('settings.category.theming.title')}
+ {#if $applicationState.isAuthenticated} +
+ {$i18n.t('settings.category.data.title')} -
- {$i18n.t('settings.category.data.title')} - -
- {#if !$applicationState.isAuthenticated} +
{$i18n.t('settings.category.data.import_data_button')} - {/if} - - {$i18n.t('settings.category.data.export_data_button')} - + + + {$i18n.t('settings.category.data.export_data_button')} + + + + + Delete account + + + + +
+ + Delete account +
+
+

+ Are you sure you want to delete your account? This action is + irreversible and all data will be lost. +

+ + + +
+
+
-
+ {/if} diff --git a/src/routes/settings/firstlogin/+page.svelte b/src/routes/settings/firstlogin/+page.svelte new file mode 100644 index 00000000..1ca94249 --- /dev/null +++ b/src/routes/settings/firstlogin/+page.svelte @@ -0,0 +1,72 @@ + + + + {#if $createProfileMutation.isIdle} +
+
+

{$i18n.t('profile.complete.title')}

+

{$i18n.t('profile.complete.description')}

+
+ +
+ {/if} + + {#if $createProfileMutation.isPending} + + {$i18n.t('profile.create.pending.title')} + {$i18n.t('profile.create.pending.description')} + + {/if} + + {#if $createProfileMutation.isError} + + {$i18n.t('profile.create.error.title')} + {error} + + {/if} +
diff --git a/src/routes/settings/import/+page.svelte b/src/routes/settings/import/+page.svelte index 10cd97f2..13326f33 100644 --- a/src/routes/settings/import/+page.svelte +++ b/src/routes/settings/import/+page.svelte @@ -4,7 +4,6 @@ import IconButton from '$lib/components/ui/icon-button/IconButton.svelte'; import { mdiAlert, mdiFile, mdiImport } from '@mdi/js'; import { Tabs, TabsList, TabsTrigger } from '$lib/components/ui/tabs'; - import ImporterService, { type ImporterServices } from '$lib/services/importer'; import { Button } from '$lib/components/ui/button'; import { AlertDialog, @@ -19,11 +18,12 @@ import { goto } from '$app/navigation'; import Text from '$lib/components/typography/Text.svelte'; import i18n from '$lib/services/i18n'; + import BackendService from '$lib/services/backend'; import { userProfile } from '$lib/store/user_profile'; - import { applicationState } from '$lib/store/application_state'; - let value: ImporterServices = 'dvalin'; - const importerService = new ImporterService(); + let value: 'dvalin' | 'paimon' = 'dvalin'; + const backend = BackendService.getInstance(); + const syncUserProfile = backend.user.syncUserProfile(); let file: File | undefined = undefined; let dialogOpen = false; @@ -31,7 +31,7 @@ let element = document.createElement('input'); element.type = 'file'; element.style.display = 'none'; - element.onchange = (event) => { + element.onchange = (event: Event) => { const target = event.target as HTMLInputElement; if (target.files && target.files.length === 1) { file = target.files[0]; @@ -47,36 +47,39 @@ dialogOpen = false; if (file !== undefined) { file.text().then((fileContent) => { - try { - let data = JSON.parse(fileContent); - const importedUserProfile = importerService - .getImporterService(value) - .import(data); - - userProfile.set(importedUserProfile); - toast.success('Imported successfully!', { - description: - 'Your data has been imported successfully and stored locally in your Browser' - }); - goto('/settings'); - } catch (e) { - toast.error('An error happened!', { - description: e.message - }); + let data = JSON.parse(fileContent); + if (!$userProfile.wishes) { + throw new Error('No wishes found, please import from dvalin first'); } + Object.entries($userProfile.wishes).forEach(([key, array]) => { + if (!array || array.length === 0) { + throw new Error('No wishes found, please import from dvalin first' + key); + } + }); + $syncUserProfile.mutateAsync( + { + file: data, + format: value + }, + { + onError: (error) => { + console.error(error); + }, + onSuccess: () => { + toast.success('Imported successfully!', { + description: + 'Your data has been send to the server and will be processed soon. You will be redirected to the settings page.' + }); + goto('/settings'); + } + } + ); }); } }; - - - Imports are temporarily disabled while being logged in - - + Dval.in diff --git a/src/routes/wish-statistics/import/+page.ts b/src/routes/wish-statistics/import/+page.ts index a6448783..4cfd0009 100644 --- a/src/routes/wish-statistics/import/+page.ts +++ b/src/routes/wish-statistics/import/+page.ts @@ -7,7 +7,7 @@ export async function load({ parent, fetch }: PageLoadEvent) { const { queryClient, backend } = await parent(); await queryClient.prefetchQuery({ - queryKey: ['fetchHoyoWishhistoryStatus', get(applicationState).isAuthenticated], + queryKey: ['fetchHoyoWishStatus', get(applicationState).isAuthenticated], queryFn: async () => ( await fetch(backend.hoyo.getHoyoWishHistoryStatusUrl(), { diff --git a/src/routes/wish-statistics/overview/+page.svelte b/src/routes/wish-statistics/overview/+page.svelte index aadfb048..f3818e94 100644 --- a/src/routes/wish-statistics/overview/+page.svelte +++ b/src/routes/wish-statistics/overview/+page.svelte @@ -24,10 +24,11 @@ import i18n from '$lib/services/i18n'; import { userProfile } from '$lib/store/user_profile'; import { applicationState } from '$lib/store/application_state'; + import { get } from 'svelte/store'; let wishData: IMappedWishes = {}; - const wishes: IWishes | undefined = $userProfile.wishes; - + // TODO : to refactor + const wishes: IWishes | undefined = get(userProfile).wishes; if (wishes !== undefined) { Object.keys(wishes).forEach((key: string) => { if (isWishBannerKey(key)) { diff --git a/vite.config.js b/vite.config.js index 236cc402..833e3cca 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,6 +3,10 @@ import { defineConfig } from 'vite'; import { SvelteKitPWA } from '@vite-pwa/sveltekit'; export default defineConfig({ + preview: { + host: true, + port: 8080 + }, plugins: [ sveltekit(), SvelteKitPWA({