diff --git a/apps/web/src/apps/auth/login/index.tsx b/apps/web/src/apps/auth/login/index.tsx index fb9b00c..4667697 100644 --- a/apps/web/src/apps/auth/login/index.tsx +++ b/apps/web/src/apps/auth/login/index.tsx @@ -1,3 +1,315 @@ +import { TextInput, PasswordInput, Checkbox, Button, Divider } from '@repo/ui/components'; +import { Mail, Lock, LogIn, Apple, Globe, MessageCircle } from 'lucide-react'; + +const GoogleIcon = (props: any) => ( + + + + + + +); + +const MicrosoftIcon = (props: any) => ( + + + + + + +); + +const AndroidIcon = (props: any) => ( + + + + +); + +const ChromeIcon = (props: any) => ( + + + + + + +); + +const Ring = ({ size }: { size: number }) => ( +
+); + +const IconCircle = ({ x, y, children }: { x: number; y: number; children: React.ReactNode }) => ( +
+ {children} +
+); + export default function LoginPage() { - return
Login Page
; + return ( +
+ {/* Left Column - Login Form */} +
+ {/* Logo (Top Left) */} +
+ {/* Image Logo (Uncomment and set src to your logo path) */} + {/* Logo */} + + {/* Text Logo (Remove if using image) */} +
+ E +
+
Eigen
+
+ +
+ {/* Header */} +
+
+
+
+
+ +
+
+ +

+ Login to your account! +

+

+ Enter your registered email address and password to login! +

+ + {/* Form */} +
+ } + size="md" + styles={{ + label: { color: '#64748b', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }, + input: { borderRadius: '10px', border: '1px solid #e2e8f0', color: '#1e293b' }, + }} + /> + + } + size="md" + styles={{ + label: { color: '#64748b', fontSize: '13px', fontWeight: 500, marginBottom: '6px' }, + input: { borderRadius: '10px', border: '1px solid #e2e8f0', color: '#1e293b' }, + }} + /> + + + + + + + +
+ + + +
+
+
+
+ + {/* Right Column - Graphic */} +
+ {/* 1. Glossy & Glassmorphic Orbs */} +
+
+
+

+ Write Better{' '} + + Everywhere + +

+ + {/* Graphics Area */} +
+ {/* Concentric Rings */} + + + + + + {/* Center Logo */} +
+
+ {/* Glowing backdrop */} +
+ {/* Custom P/Pen Logo from reference */} + + + + + +
+
+ + {/* Orbiting Icons */} + + {/* Ring 1 (r=100) */} +
+ + + +
+ +
+ + + +
+ + {/* Ring 2 (r=150) */} +
+ + + +
+ +
+ + y! + +
+ + {/* Ring 3 (r=210) */} +
+ + + +
+ +
+ + + +
+ + {/* Ring 4 (r=270) */} +
+ + + +
+
+ +

+ Compatible with{' '} + Gmail, Outlook Web, LinkedIn and most web editors for + a smooth writing experience anywhere online. +

+ + {/* Pagination Indicators */} +
+
+
+
+
+
+
+ ); } diff --git a/apps/web/src/core/storage/local/index.ts b/apps/web/src/core/storage/local/index.ts index 8e9b3ed..c998ef5 100644 --- a/apps/web/src/core/storage/local/index.ts +++ b/apps/web/src/core/storage/local/index.ts @@ -5,7 +5,7 @@ export const AppStorageKey = { THEME: 'app_theme', ACCESS_TOKEN: 'access_token', REFRESH_TOKEN: 'refresh_token', - USER_ID: 'u_id', + USER_ID: 'uid', SIDEBAR_OPEN_MENUS: 'sidebar_open_menus', } as const; @@ -21,18 +21,43 @@ export const AppDatabaseKey = { export type AppDatabaseKeyValue = (typeof AppDatabaseKey)[keyof typeof AppDatabaseKey]; +function getUserId(userIdKey: string) { + const rawId = localStorage.getItem(userIdKey); + if (!rawId) return null; + + try { + return JSON.parse(rawId); + } catch (error) { + console.error('Failed to parse User ID:', error); + return null; + } +} + export const APP_STORAGE_ENCRYPTED_KEYS = new Set([ - AppStorageKey.USER_ID, AppStorageKey.ACCESS_TOKEN, AppStorageKey.REFRESH_TOKEN, ]); export const APP_STORAGE_PLAIN_KEYS = new Set([ + AppStorageKey.USER_ID, AppStorageKey.LANGUAGE, AppStorageKey.THEME, AppStorageKey.SIDEBAR_OPEN_MENUS, ]); +export const APP_STORAGE_PERSONALIZED_KEYS = new Set([ + AppStorageKey.LANGUAGE, + AppStorageKey.THEME, + AppStorageKey.SIDEBAR_OPEN_MENUS, +]); + +export const appStorage = createLocalStorage({ + encryptedKeys: APP_STORAGE_ENCRYPTED_KEYS, + plainTextKeys: APP_STORAGE_PLAIN_KEYS, + personalizedKeys: APP_STORAGE_PERSONALIZED_KEYS, + getUserId: () => getUserId(AppStorageKey.USER_ID) || null, +}); + export const APP_DATABASE_ENCRYPTED_KEYS = new Set([]); export const APP_DATABASE_PLAIN_KEYS = new Set([ @@ -43,14 +68,19 @@ export const APP_DATABASE_PLAIN_KEYS = new Set([ AppDatabaseKey.BOOKMARK_PAGE, ]); -export const appStorage = createLocalStorage({ - encryptedKeys: APP_STORAGE_ENCRYPTED_KEYS, - plainTextKeys: APP_STORAGE_PLAIN_KEYS, -}); +export const APP_DATABASE_PERSONALIZED_KEYS = new Set([ + AppDatabaseKey.USER_PROFILE, + AppDatabaseKey.OFFLINE_DRAFT, + AppDatabaseKey.SYSTEM_SETTINGS, + AppDatabaseKey.HISTORY_PAGE, + AppDatabaseKey.BOOKMARK_PAGE, +]); export const appDatabase = createIndexedDB({ dbName: 'e_apps_db', storeName: 'web_store', encryptedKeys: APP_DATABASE_ENCRYPTED_KEYS, plainTextKeys: APP_DATABASE_PLAIN_KEYS, + personalizedKeys: APP_DATABASE_PERSONALIZED_KEYS, + getUserId: () => getUserId(AppStorageKey.USER_ID) || null, }); diff --git a/apps/web/src/main.css b/apps/web/src/main.css index 1a6a0b5..1e9abaa 100644 --- a/apps/web/src/main.css +++ b/apps/web/src/main.css @@ -1,3 +1,32 @@ @import 'tailwindcss'; @import '@repo/ui/theme.css'; @source "../../../packages/ui/src"; + +/* ========================================= + ANIMATION: FLOATING + ========================================= */ + +/* --- Keyframes Definition --- */ +@keyframes floating { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); /* Peak float distance (adjust as needed) */ + } +} + +/* --- Main Utility Class --- */ +.animate-floating { + animation: floating 4s ease-in-out infinite; +} + +/* --- Speed Variations (Optional) --- */ +.animate-floating-fast { + animation: floating 3s ease-in-out infinite; +} + +.animate-floating-slow { + animation: floating 5s ease-in-out infinite; +} diff --git a/packages/core-storage/src/indexed-db/indexed-db.service.ts b/packages/core-storage/src/indexed-db/indexed-db.service.ts index c04cd85..dd36dca 100644 --- a/packages/core-storage/src/indexed-db/indexed-db.service.ts +++ b/packages/core-storage/src/indexed-db/indexed-db.service.ts @@ -37,7 +37,7 @@ function openDatabase(dbName: string, storeName: string, version: number): Promi /** * Execute a single IndexedDB transaction and return the result. - * Handles open → transaction → request → close lifecycle cleanly. + * Resolves only when the entire transaction is completed to prevent silent failures. */ function withTransaction( db: IDBDatabase, @@ -48,10 +48,20 @@ function withTransaction( return new Promise((resolve, reject) => { const tx = db.transaction(storeName, mode); const store = tx.objectStore(storeName); + let requestResult: R; + const request = operation(store); - request.onsuccess = () => resolve(request.result); + request.onsuccess = () => { + requestResult = request.result; + }; + request.onerror = () => reject(request.error); + + // Resolve promise ONLY after transaction is successfully committed + tx.oncomplete = () => resolve(requestResult); + tx.onerror = () => reject(tx.error); + tx.onabort = () => reject(tx.error); }); } @@ -67,6 +77,8 @@ export class IndexedDBService implements IStorageService; private readonly plainTextKeys: Set; + private readonly personalizedKeys?: Set; + private readonly getUserId?: () => string | null | undefined; private dbPromise: Promise | null = null; constructor(config?: IndexedDBConfig, encryptionUtils?: EncryptionUtils) { @@ -76,6 +88,8 @@ export class IndexedDBService implements IStorageService implements IStorageService(key: TKey, value: T): Promise { this.validateKey(key); const db = await this.getDB(); + const resolvedKey = this.resolveKey(key); + const serialized = JSON.stringify(value); const payload = this.shouldEncrypt(key) ? this.encryption.encrypt(serialized) : serialized; - await withTransaction(db, this.storeName, 'readwrite', (store) => store.put(payload, key as string)); + await withTransaction(db, this.storeName, 'readwrite', (store) => store.put(payload, resolvedKey)); } async getItem(key: TKey): Promise { this.validateKey(key); const db = await this.getDB(); + const resolvedKey = this.resolveKey(key); const raw = await withTransaction( db, this.storeName, 'readonly', - (store) => store.get(key as string) as IDBRequest, + (store) => store.get(resolvedKey) as IDBRequest, ); if (raw === undefined || raw === null) return null; try { - if (this.shouldEncrypt(key)) { - const decrypted = this.encryption.decrypt(raw); - if (!decrypted) return null; - return JSON.parse(decrypted) as T; - } - return JSON.parse(raw) as T; + const decrypted = this.shouldEncrypt(key) ? this.encryption.decrypt(raw) : raw; + if (!decrypted) return null; + + return JSON.parse(decrypted) as T; } catch { - console.warn(`[core-storage] Failed to parse IndexedDB key "${key}". Removing corrupt entry.`); + console.warn(`[core-storage] Failed to parse IndexedDB key "${String(key)}". Removing corrupt entry.`); await this.removeItem(key); return null; } @@ -135,12 +158,16 @@ export class IndexedDBService implements IStorageService { this.validateKey(key); const db = await this.getDB(); - await withTransaction(db, this.storeName, 'readwrite', (store) => store.delete(key as string)); + const resolvedKey = this.resolveKey(key); + + await withTransaction(db, this.storeName, 'readwrite', (store) => store.delete(resolvedKey)); } async clear(): Promise { - const db = await this.getDB(); - await withTransaction(db, this.storeName, 'readwrite', (store) => store.clear()); + const allValidKeys = await this.keys(); + for (const key of allValidKeys) { + await this.removeItem(key); + } } async hasItem(key: TKey): Promise { @@ -150,13 +177,34 @@ export class IndexedDBService implements IStorageService { const db = await this.getDB(); - const allKeys = await withTransaction( + const rawKeys = await withTransaction( db, this.storeName, 'readonly', (store) => store.getAllKeys() as IDBRequest, ); - return allKeys as TKey[]; + + const resultSet = new Set(); + const currentUserId = this.getUserId?.() || 'guest'; + + for (const rawKey of rawKeys) { + const isGlobalKey = this.encryptedKeys.has(rawKey as TKey) || this.plainTextKeys.has(rawKey as TKey); + + if (isGlobalKey && !this.personalizedKeys?.has(rawKey as TKey)) { + resultSet.add(rawKey as TKey); + } + + if (this.personalizedKeys) { + for (const pKey of this.personalizedKeys) { + if (rawKey === `${String(pKey)}_${currentUserId}`) { + resultSet.add(pKey); + break; + } + } + } + } + + return Array.from(resultSet); } } diff --git a/packages/core-storage/src/local-storage/local-storage.service.ts b/packages/core-storage/src/local-storage/local-storage.service.ts index 7675a98..b36086d 100644 --- a/packages/core-storage/src/local-storage/local-storage.service.ts +++ b/packages/core-storage/src/local-storage/local-storage.service.ts @@ -4,6 +4,8 @@ import type { IStorageService } from '../types/storage.interface'; export interface StorageOptions { encryptedKeys?: Set; plainTextKeys?: Set; + personalizedKeys?: Set; + getUserId?: () => string | null | undefined; } /** @@ -13,11 +15,15 @@ export class LocalStorageService implements IStorageService private readonly encryption: EncryptionUtils; private readonly encryptedKeys: Set; private readonly plainTextKeys: Set; + private readonly personalizedKeys?: Set; + private readonly getUserId?: () => string | null | undefined; constructor(options?: StorageOptions, encryptionUtils?: EncryptionUtils) { this.encryption = encryptionUtils ?? EncryptionUtils.getInstance(); this.encryptedKeys = options?.encryptedKeys ?? new Set(); this.plainTextKeys = options?.plainTextKeys ?? new Set(); + this.personalizedKeys = options?.personalizedKeys; + this.getUserId = options?.getUserId; } private validateKey(key: TKey): void { @@ -26,25 +32,35 @@ export class LocalStorageService implements IStorageService } } + private resolveKey(key: TKey): string { + if (this.personalizedKeys?.has(key)) { + const userId = this.getUserId?.() || 'guest'; + return `${String(key)}_${userId}`; + } + return String(key); + } + private shouldEncrypt(key: TKey): boolean { return this.encryptedKeys.has(key); } async setItem(key: TKey, value: T): Promise { this.validateKey(key); + const resolvedKey = this.resolveKey(key); const serialized = JSON.stringify(value); if (this.shouldEncrypt(key)) { const encrypted = this.encryption.encrypt(serialized); - localStorage.setItem(key as string, encrypted); + localStorage.setItem(resolvedKey, encrypted); } else { - localStorage.setItem(key as string, serialized); + localStorage.setItem(resolvedKey, serialized); } } async getItem(key: TKey): Promise { this.validateKey(key); - const raw = localStorage.getItem(key as string); + const resolvedKey = this.resolveKey(key); + const raw = localStorage.getItem(resolvedKey); if (raw === null) return null; try { @@ -55,15 +71,16 @@ export class LocalStorageService implements IStorageService } return JSON.parse(raw) as T; } catch { - console.warn(`[core-storage] Failed to parse key "${key}". Removing corrupt entry.`); - localStorage.removeItem(key as string); + console.warn(`[core-storage] Failed to parse key "${String(key)}". Removing corrupt entry.`); + localStorage.removeItem(resolvedKey); return null; } } async removeItem(key: TKey): Promise { this.validateKey(key); - localStorage.removeItem(key as string); + const resolvedKey = this.resolveKey(key); + localStorage.removeItem(resolvedKey); } async clear(): Promise { @@ -71,7 +88,8 @@ export class LocalStorageService implements IStorageService } async hasItem(key: TKey): Promise { - return localStorage.getItem(key as string) !== null; + const resolvedKey = this.resolveKey(key); + return localStorage.getItem(resolvedKey) !== null; } async keys(): Promise { diff --git a/packages/ui/src/components/ag-grid/ag-grid-theme.ts b/packages/ui/src/components/ag-grid/ag-grid-theme.ts index a873e68..03f740a 100644 --- a/packages/ui/src/components/ag-grid/ag-grid-theme.ts +++ b/packages/ui/src/components/ag-grid/ag-grid-theme.ts @@ -52,6 +52,7 @@ export const agGridMantineTheme = themeQuartz /* ── Row Styling ────────────────────────────────────────────── */ oddRowBackgroundColor: 'var(--mantine-color-default-hover)', + // oddRowBackgroundColor: 'color-mix(in srgb, var(--mantine-color-text) 2.5%, transparent)', rowHoverColor: 'var(--mantine-primary-color-light)', selectedRowBackgroundColor: 'var(--mantine-primary-color-light)', @@ -61,6 +62,7 @@ export const agGridMantineTheme = themeQuartz /* ── Spacing ────────────────────────────────────────────────── */ spacing: 'var(--mantine-spacing-sm)', + // spacing: 'var(--mantine-spacing-xs)', }) /* ═══════════════════════════════════════════════════════════════ diff --git a/packages/ui/src/foundations/enterprise-module/hooks/use-form-draft.context.ts b/packages/ui/src/foundations/enterprise-module/hooks/use-form-draft.context.ts index 1dfc3a9..217a573 100644 --- a/packages/ui/src/foundations/enterprise-module/hooks/use-form-draft.context.ts +++ b/packages/ui/src/foundations/enterprise-module/hooks/use-form-draft.context.ts @@ -3,8 +3,6 @@ import { useState, useEffect, useCallback } from 'react'; import { useEnterpriseModuleConfigContext } from './use-module.context'; import { DraftConfig, FormPageType } from '../entities/entity'; import { - appStorage, - AppStorageKey, appDatabase, AppDatabaseKey, } from '../../../../../../apps/web/src/core/storage/local'; @@ -19,36 +17,28 @@ export function useFormDraftContext({ config }: FormDraftContextProps) { const { config: moduleConfig } = useEnterpriseModuleConfigContext(); const [hasDraft, setHasDraft] = useState(false); const [draftData, setDraftData] = useState(null); - const [userID, setUserID] = useState('UNRESOLVED_PRINCIPAL'); const isEnabled = config?.enableDraft === true; // Use a generic 'CREATE' key for both CREATE and DUPLICATE so that drafts saved // during DUPLICATE can be restored when entering a new CREATE form. - const draftKey = `${userID}:draft:${moduleConfig.moduleKey}:CREATE`; + const draftKey = `draft:${moduleConfig.moduleKey}:CREATE`; // Check for existing draft on mount useEffect(() => { if (!isEnabled) return; - // Fetch user ID asynchronously using appStorage - appStorage.getItem(AppStorageKey.USER_ID).then((id: any) => { - const resolvedUserId = id || 'UNRESOLVED_PRINCIPAL'; - setUserID(resolvedUserId); - - const resolvedDraftKey = `${resolvedUserId}:draft:${moduleConfig.moduleKey}:CREATE`; - appDatabase - .getItem(AppDatabaseKey.OFFLINE_DRAFT) - .then((allDrafts: any) => { - const drafts = allDrafts || {}; - if (drafts[resolvedDraftKey]) { - setDraftData(drafts[resolvedDraftKey]); - setHasDraft(true); - } - }) - .catch((err) => { - console.error('[Draft Recovery] Failed to read from appDatabase:', err); - }); - }); - }, [isEnabled, moduleConfig.moduleKey]); + appDatabase + .getItem(AppDatabaseKey.OFFLINE_DRAFT) + .then((allDrafts: any) => { + const drafts = allDrafts || {}; + if (drafts[draftKey]) { + setDraftData(drafts[draftKey]); + setHasDraft(true); + } + }) + .catch((err) => { + console.error('[Draft Recovery] Failed to read from appDatabase:', err); + }); + }, [isEnabled, draftKey]); const saveDraft = useCallback( (data: any) => {