diff --git a/package-lock.json b/package-lock.json index 6b76bb0..29c5c36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,8 @@ "react-error-boundary": "^6.0.1", "react-icons": "^5.5.0", "react-router-dom": "6.27.0", - "uqr": "^0.1.2" + "uqr": "^0.1.2", + "zustand": "^5.0.9" }, "devDependencies": { "@eslint/compat": "^1.3.2", @@ -12358,6 +12359,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", + "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index e009b07..5756dd6 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "react-error-boundary": "^6.0.1", "react-icons": "^5.5.0", "react-router-dom": "6.27.0", - "uqr": "^0.1.2" + "uqr": "^0.1.2", + "zustand": "^5.0.9" }, "devDependencies": { "@eslint/compat": "^1.3.2", diff --git a/src/features/cryptohapp/decrypt/index.ts b/src/features/cryptohapp/decrypt/index.ts index 81dc5c9..b601394 100644 --- a/src/features/cryptohapp/decrypt/index.ts +++ b/src/features/cryptohapp/decrypt/index.ts @@ -1,2 +1,2 @@ +export * from './model' export { DecryptCard } from './ui/decrypt-card' - diff --git a/src/features/cryptohapp/decrypt/model/index.ts b/src/features/cryptohapp/decrypt/model/index.ts new file mode 100644 index 0000000..5877239 --- /dev/null +++ b/src/features/cryptohapp/decrypt/model/index.ts @@ -0,0 +1,2 @@ +export * from './interfaces' +export * from './use-decrypt-keys-store' diff --git a/src/features/cryptohapp/decrypt/model/interfaces/action.interface.ts b/src/features/cryptohapp/decrypt/model/interfaces/action.interface.ts new file mode 100644 index 0000000..7332cb9 --- /dev/null +++ b/src/features/cryptohapp/decrypt/model/interfaces/action.interface.ts @@ -0,0 +1,10 @@ +import type { IDecryptKey } from './state.interface' + +export interface IActions { + actions: { + addKey: (key: Omit) => void + deleteKey: (id: string) => void + selectKey: (id: null | string) => void + updateKey: (id: string, key: Partial>) => void + } +} diff --git a/src/features/cryptohapp/decrypt/model/interfaces/index.ts b/src/features/cryptohapp/decrypt/model/interfaces/index.ts new file mode 100644 index 0000000..3aa4bbe --- /dev/null +++ b/src/features/cryptohapp/decrypt/model/interfaces/index.ts @@ -0,0 +1,2 @@ +export type { IActions } from './action.interface' +export type { IDecryptKey, IState } from './state.interface' diff --git a/src/features/cryptohapp/decrypt/model/interfaces/state.interface.ts b/src/features/cryptohapp/decrypt/model/interfaces/state.interface.ts new file mode 100644 index 0000000..e9cc16e --- /dev/null +++ b/src/features/cryptohapp/decrypt/model/interfaces/state.interface.ts @@ -0,0 +1,10 @@ +export interface IDecryptKey { + content: string + id: string + name: string +} + +export interface IState { + keys: IDecryptKey[] + selectedKeyId: null | string +} diff --git a/src/features/cryptohapp/decrypt/model/use-decrypt-keys-store.ts b/src/features/cryptohapp/decrypt/model/use-decrypt-keys-store.ts new file mode 100644 index 0000000..5ab77ae --- /dev/null +++ b/src/features/cryptohapp/decrypt/model/use-decrypt-keys-store.ts @@ -0,0 +1,59 @@ +import { createJSONStorage, devtools, persist } from 'zustand/middleware' +import { create } from 'zustand' + +import type { IActions, IState } from './interfaces' + +const generateId = () => { + const salt = Math.random().toString(36).substring(2, 8) + return `${Date.now()}-${salt}` +} + +export const useDecryptKeysStore = create()( + persist( + devtools( + (set) => ({ + keys: [], + selectedKeyId: null, + actions: { + addKey: (key) => { + const newKey = { ...key, id: generateId() } + set((state) => ({ + keys: [...state.keys, newKey], + selectedKeyId: newKey.id + })) + }, + deleteKey: (id) => { + set((state) => ({ + keys: state.keys.filter((k) => k.id !== id), + selectedKeyId: state.selectedKeyId === id ? null : state.selectedKeyId + })) + }, + selectKey: (id) => { + set({ selectedKeyId: id }) + }, + updateKey: (id, updates) => { + set((state) => ({ + keys: state.keys.map((k) => (k.id === id ? { ...k, ...updates } : k)) + })) + } + } + }), + { anonymousActionType: 'decryptKeysStore', name: 'decryptKeysStore' } + ), + { + name: 'decryptKeysStore', + partialize: (state) => ({ + keys: state.keys, + selectedKeyId: state.selectedKeyId + }), + storage: createJSONStorage(() => localStorage), + version: 1 + } + ) +) + +export const useDecryptKeys = () => useDecryptKeysStore((state) => state.keys) +export const useSelectedKeyId = () => useDecryptKeysStore((state) => state.selectedKeyId) +export const useSelectedKey = () => + useDecryptKeysStore((state) => state.keys.find((k) => k.id === state.selectedKeyId) ?? null) +export const useDecryptKeysActions = () => useDecryptKeysStore((state) => state.actions) diff --git a/src/features/cryptohapp/decrypt/ui/add-key-modal.tsx b/src/features/cryptohapp/decrypt/ui/add-key-modal.tsx new file mode 100644 index 0000000..8ceeec0 --- /dev/null +++ b/src/features/cryptohapp/decrypt/ui/add-key-modal.tsx @@ -0,0 +1,102 @@ +import { Button, Group, Modal, Stack, Textarea, TextInput } from '@mantine/core' +import { IconKey, IconPlus } from '@tabler/icons-react' +import { useState } from 'react' + +import { BaseOverlayHeader } from '@shared/ui' + +import { useDecryptKeysActions } from '../model' +import styles from './decrypt-card.module.css' + +interface AddKeyModalProps { + onClose: () => void + opened: boolean +} + +const PLACEHOLDER_KEY = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA... +-----END RSA PRIVATE KEY-----` + +export function AddKeyModal({ opened, onClose }: AddKeyModalProps) { + const [name, setName] = useState('') + const [content, setContent] = useState('') + const { addKey } = useDecryptKeysActions() + + const handleSubmit = () => { + if (!name.trim() || !content.trim()) return + + addKey({ content: content.trim(), name: name.trim() }) + setName('') + setContent('') + onClose() + } + + const handleClose = () => { + setName('') + setContent('') + onClose() + } + + return ( + + } + > + + setName(e.currentTarget.value)} + placeholder="e.g. TestKey" + value={name} + /> + +