Initial commit: G3FO recurring investment schedule frontend
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
export const APP_LOCALES = ['zh-CN', 'zh-TW', 'en-US'] as const
|
||||
|
||||
export type AppLocale = (typeof APP_LOCALES)[number]
|
||||
|
||||
export const DEFAULT_LOCALE: AppLocale = 'zh-TW'
|
||||
|
||||
export function isAppLocale(value: string | null): value is AppLocale {
|
||||
return value !== null && (APP_LOCALES as readonly string[]).includes(value)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
|
||||
export const URL_PARAM_CLIENT_ID = 'clientId'
|
||||
export const URL_PARAM_ACCOUNT_ID = 'accountId'
|
||||
|
||||
export interface UrlAccountContext {
|
||||
clientId: string | null
|
||||
accountId: string | null
|
||||
}
|
||||
|
||||
function readQueryValue(query: LocationQuery, key: string): string | null {
|
||||
const raw = query[key]
|
||||
if (raw === undefined || raw === null) {
|
||||
return null
|
||||
}
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return null
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function readSearchParam(search: string, key: string): string | null {
|
||||
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search)
|
||||
const value = params.get(key)
|
||||
return value && value.trim() !== '' ? value : null
|
||||
}
|
||||
|
||||
export function parseAccountContextFromSearch(search: string): UrlAccountContext {
|
||||
return {
|
||||
clientId: readSearchParam(search, URL_PARAM_CLIENT_ID),
|
||||
accountId: readSearchParam(search, URL_PARAM_ACCOUNT_ID),
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAccountContextFromQuery(query: LocationQuery): UrlAccountContext {
|
||||
return {
|
||||
clientId: readQueryValue(query, URL_PARAM_CLIENT_ID),
|
||||
accountId: readQueryValue(query, URL_PARAM_ACCOUNT_ID),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { DEFAULT_LOCALE, isAppLocale, type AppLocale } from '@/config/appLocale'
|
||||
import { APP_THEMES, DEFAULT_THEME, type AppTheme } from '@/theme/types'
|
||||
|
||||
export const URL_PARAM_LANG = 'lang'
|
||||
export const URL_PARAM_THEME = 'theme'
|
||||
|
||||
export interface AppPreferences {
|
||||
locale: AppLocale
|
||||
theme: AppTheme
|
||||
}
|
||||
|
||||
function readQueryValue(query: LocationQuery, key: string): string | undefined {
|
||||
const raw = query[key]
|
||||
if (raw === undefined || raw === null) {
|
||||
return undefined
|
||||
}
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return undefined
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function normalizeTheme(value: string | undefined): AppTheme {
|
||||
if (!value) {
|
||||
return DEFAULT_THEME
|
||||
}
|
||||
const lower = value.toLowerCase()
|
||||
if (lower === 'day' || lower === 'light') {
|
||||
return 'light'
|
||||
}
|
||||
if (lower === 'night' || lower === 'dark') {
|
||||
return 'dark'
|
||||
}
|
||||
return APP_THEMES.includes(lower as AppTheme) ? (lower as AppTheme) : DEFAULT_THEME
|
||||
}
|
||||
|
||||
function normalizeLocale(value: string | undefined): AppLocale {
|
||||
if (!value) {
|
||||
return DEFAULT_LOCALE
|
||||
}
|
||||
const normalized = value.replace('_', '-')
|
||||
if (isAppLocale(normalized)) {
|
||||
return normalized
|
||||
}
|
||||
return DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
export function parsePreferencesFromSearch(search: string): AppPreferences {
|
||||
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search)
|
||||
const lang = params.get(URL_PARAM_LANG) ?? params.get('locale')
|
||||
const theme = params.get(URL_PARAM_THEME)
|
||||
return {
|
||||
locale: normalizeLocale(lang ?? undefined),
|
||||
theme: normalizeTheme(theme ?? undefined),
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePreferencesFromQuery(query: LocationQuery): AppPreferences {
|
||||
const lang = readQueryValue(query, URL_PARAM_LANG) ?? readQueryValue(query, 'locale')
|
||||
const theme = readQueryValue(query, URL_PARAM_THEME)
|
||||
return {
|
||||
locale: normalizeLocale(lang),
|
||||
theme: normalizeTheme(theme),
|
||||
}
|
||||
}
|
||||
|
||||
export function preferencesToQuery(
|
||||
preferences: AppPreferences,
|
||||
existing: LocationQuery = {},
|
||||
): Record<string, string> {
|
||||
const next: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(existing)) {
|
||||
if (key === URL_PARAM_LANG || key === 'locale' || key === URL_PARAM_THEME) {
|
||||
continue
|
||||
}
|
||||
if (value !== undefined && value !== null) {
|
||||
const v = Array.isArray(value) ? value[0] : value
|
||||
if (v !== undefined && v !== null && v !== '') {
|
||||
next[key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
next[URL_PARAM_LANG] = preferences.locale
|
||||
next[URL_PARAM_THEME] = preferences.theme
|
||||
return next
|
||||
}
|
||||
Reference in New Issue
Block a user