diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 0000000..8d930d7 --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,95 @@ +# Configuration Files + +This application uses JSON configuration files to manage strings and settings, making it easy to update text, languages, and other app configurations without modifying the TypeScript/TSX code. + +## Configuration Files + +### `/src/config/strings.json` +Contains all user-facing text strings used throughout the application. + +**Structure:** +- `app` - Main application strings (title, header, search) +- `emptyState` - Empty state component text +- `noResults` - No search results text +- `snippetCard` - Snippet card component text and aria labels +- `snippetDialog` - Dialog form labels, placeholders, and error messages +- `snippetViewer` - Viewer component button labels +- `toast` - Toast notification messages + +**Example usage in components:** +```typescript +import { strings } from '@/lib/config' + +// Access strings +

{strings.app.header.title}

+

{strings.emptyState.description}

+``` + +### `/src/config/app-config.json` +Contains application configuration settings and data. + +**Structure:** +- `languages` - Array of supported programming languages +- `languageColors` - Color schemes for each language badge +- `previewEnabledLanguages` - Languages that support live preview +- `defaultLanguage` - Default language selection +- `codePreviewMaxLength` - Maximum characters to show in card preview +- `copiedTimeout` - Milliseconds to show "copied" state + +**Example usage in components:** +```typescript +import { appConfig } from '@/lib/config' + +// Access configuration +const maxLength = appConfig.codePreviewMaxLength +const timeout = appConfig.copiedTimeout +``` + +## Helper Functions + +### `/src/lib/config.ts` +Provides utility functions and exports for accessing configuration data. + +**Exports:** +- `strings` - All string data from strings.json +- `appConfig` - All configuration from app-config.json +- `getLanguageColor(language)` - Get combined color classes for a language +- `LANGUAGES` - Array of language options +- `LANGUAGE_COLORS` - Map of language to color classes + +**Example:** +```typescript +import { getLanguageColor, LANGUAGES } from '@/lib/config' + +const colorClass = getLanguageColor('JavaScript') +// Returns: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30" +``` + +## Updating Text and Configuration + +### To change text: +1. Edit `/src/config/strings.json` +2. No TypeScript changes needed +3. Changes are reflected immediately + +### To add a new language: +1. Add to `languages` array in `/src/config/app-config.json` +2. Add color scheme to `languageColors` object +3. No component changes needed + +### To change colors: +1. Edit color values in `languageColors` in `/src/config/app-config.json` +2. Use Tailwind color classes (bg-, text-, border-) + +### To change timeouts or limits: +1. Edit values in `/src/config/app-config.json` +2. `codePreviewMaxLength` - Characters shown in preview +3. `copiedTimeout` - How long "Copied!" message shows + +## Benefits + +✅ **Easy Localization** - All strings in one place for translation +✅ **No Code Changes** - Update text without touching TSX files +✅ **Type Safety** - TypeScript still validates usage +✅ **Maintainability** - Centralized configuration +✅ **Consistency** - Single source of truth for strings and settings diff --git a/src/App.tsx b/src/App.tsx index 6e790a5..900b367 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { SnippetDialog } from '@/components/SnippetDialog' import { SnippetViewer } from '@/components/SnippetViewer' import { EmptyState } from '@/components/EmptyState' import { toast } from 'sonner' +import { strings } from '@/lib/config' function App() { const [snippets, setSnippets] = useKV('snippets', []) @@ -39,7 +40,7 @@ function App() { : s ) ) - toast.success('Snippet updated successfully') + toast.success(strings.toast.snippetUpdated) } else { const newSnippet: Snippet = { ...snippetData, @@ -48,7 +49,7 @@ function App() { updatedAt: Date.now(), } setSnippets((current) => [...(current ?? []), newSnippet]) - toast.success('Snippet created successfully') + toast.success(strings.toast.snippetCreated) } setEditingSnippet(null) } @@ -60,12 +61,12 @@ function App() { const handleDeleteSnippet = (id: string) => { setSnippets((current) => (current ?? []).filter((s) => s.id !== id)) - toast.success('Snippet deleted') + toast.success(strings.toast.snippetDeleted) } const handleCopySnippet = (code: string) => { navigator.clipboard.writeText(code) - toast.success('Code copied to clipboard') + toast.success(strings.toast.codeCopied) } const handleViewSnippet = (snippet: Snippet) => { @@ -99,10 +100,10 @@ function App() {

- Code Snippets + {strings.app.header.title}

- {currentSnippets.length} {currentSnippets.length === 1 ? 'snippet' : 'snippets'} saved + {currentSnippets.length} {currentSnippets.length === 1 ? strings.app.header.snippetCount.singular : strings.app.header.snippetCount.plural} saved

@@ -112,7 +113,7 @@ function App() { className="gap-2 w-full sm:w-auto" > - New Snippet + {strings.app.header.newSnippetButton} @@ -121,7 +122,7 @@ function App() {
setSearchQuery(e.target.value)} className="pl-10 h-11" @@ -138,9 +139,9 @@ function App() { ) : filteredSnippets.length === 0 ? (
-

No snippets found

+

{strings.noResults.title}

- Try adjusting your search query + {strings.noResults.description}

) : ( diff --git a/src/ErrorFallback.tsx b/src/ErrorFallback.tsx index 5550b44..c256851 100644 --- a/src/ErrorFallback.tsx +++ b/src/ErrorFallback.tsx @@ -1,110 +1,99 @@ -import { useState } from "react"; -import { Alert, AlertTitle, AlertDescription } from "./components/ui/alert"; -import { AIErrorHelper } from "./components/AIEr -import { AlertTriangleIcon, RefreshCwIcon, ChevronDownIcon, -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./components/ui/collapsible"; -import { AlertTriangleIcon, RefreshCwIcon, ChevronDownIcon, ChevronUpIcon, CopyIcon, CheckIcon } from "lucide-react"; - -interface ErrorFallbackProps { - - if (import.meta.env.DEV) throw - - - - navigator.clipboard.writeText(error - - - const [copied, setCopied] = useState(false); - - const errorDetails = `Error: ${error.message}\n\nStack Trace:\n${error.stack || 'No stack trace available'}`; - - const handleCopy = () => { - navigator.clipboard.writeText(errorDetails); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - return ( -
-
- - - This spark has encountered a runtime error - onClick={handl - > - <> - - - - - - - {error.message} - - - <> - - )} - - {error.s -
- Tr - -
-} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +import { useState } from "react"; +import { Alert, AlertTitle, AlertDescription } from "./components/ui/alert"; +import { AIErrorHelper } from "./components/AIErrorHelper"; +import { Button } from "./components/ui/button"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./components/ui/collapsible"; +import { AlertTriangleIcon, RefreshCwIcon, ChevronDownIcon, ChevronUpIcon, CopyIcon, CheckIcon } from "lucide-react"; + +interface ErrorFallbackProps { + error: Error; +} + +export function ErrorFallback({ error }: ErrorFallbackProps) { + if (import.meta.env.DEV) throw error; + + const [isStackOpen, setIsStackOpen] = useState(false); + const [copied, setCopied] = useState(false); + + const errorDetails = `Error: ${error.message}\n\nStack Trace:\n${error.stack || 'No stack trace available'}`; + + const handleCopy = () => { + navigator.clipboard.writeText(errorDetails); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+ + + This spark has encountered a runtime error + +
+ + {error.message} + + +
+ + + + + + +
+
+                    {error.stack || 'No stack trace available'}
+                  
+
+
+
+
+
+ + + + +
+
+ ); +} diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx index ecd9107..5e3144e 100644 --- a/src/components/EmptyState.tsx +++ b/src/components/EmptyState.tsx @@ -1,5 +1,6 @@ import { Code } from '@phosphor-icons/react' import { Button } from '@/components/ui/button' +import { strings } from '@/lib/config' interface EmptyStateProps { onCreateClick: () => void @@ -11,13 +12,13 @@ export function EmptyState({ onCreateClick }: EmptyStateProps) {
-

No snippets yet

+

{strings.emptyState.title}

- Start building your code snippet library. Save reusable code for quick access anytime. + {strings.emptyState.description}

) diff --git a/src/components/SnippetCard.tsx b/src/components/SnippetCard.tsx index 3bb8efa..6cb0609 100644 --- a/src/components/SnippetCard.tsx +++ b/src/components/SnippetCard.tsx @@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { Copy, Pencil, Trash, Eye } from '@phosphor-icons/react' import { Snippet, LANGUAGE_COLORS } from '@/lib/types' +import { strings, appConfig } from '@/lib/config' interface SnippetCardProps { snippet: Snippet @@ -20,7 +21,7 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp try { const code = snippet?.code || '' const description = snippet?.description || '' - const maxLength = 150 + const maxLength = appConfig.codePreviewMaxLength const isTruncated = code.length > maxLength const displayCode = isTruncated ? code.slice(0, maxLength) + '...' : code @@ -46,7 +47,7 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp e.stopPropagation() onCopy(snippetData.fullCode) setIsCopied(true) - setTimeout(() => setIsCopied(false), 2000) + setTimeout(() => setIsCopied(false), appConfig.copiedTimeout) } const handleEdit = (e: React.MouseEvent) => { @@ -66,7 +67,7 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp if (!snippet) { return ( -

Error loading snippet

+

{strings.snippetCard.errorMessage}

) } @@ -101,7 +102,7 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp {snippetData.isTruncated && (

- Click to view full code... + {strings.snippetCard.viewFullCode}

)} @@ -115,7 +116,7 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp className="gap-2" > - View + {strings.snippetCard.viewButton}
@@ -124,16 +125,16 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp size="sm" onClick={handleCopy} className="gap-2" - aria-label="Copy code" + aria-label={strings.snippetCard.ariaLabels.copy} > - {isCopied ? 'Copied!' : 'Copy'} + {isCopied ? strings.snippetCard.copiedButton : strings.snippetCard.copyButton} @@ -142,7 +143,7 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp size="sm" onClick={handleDelete} className="text-destructive hover:text-destructive hover:bg-destructive/10" - aria-label="Delete snippet" + aria-label={strings.snippetCard.ariaLabels.delete} > diff --git a/src/components/SnippetDialog.tsx b/src/components/SnippetDialog.tsx index 2975d88..80b1ae7 100644 --- a/src/components/SnippetDialog.tsx +++ b/src/components/SnippetDialog.tsx @@ -21,6 +21,7 @@ import { } from '@/components/ui/select' import { Snippet, LANGUAGES } from '@/lib/types' import { MonacoEditor } from '@/components/MonacoEditor' +import { strings, appConfig } from '@/lib/config' interface SnippetDialogProps { open: boolean @@ -32,7 +33,7 @@ interface SnippetDialogProps { export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: SnippetDialogProps) { const [title, setTitle] = useState('') const [description, setDescription] = useState('') - const [language, setLanguage] = useState('JavaScript') + const [language, setLanguage] = useState(appConfig.defaultLanguage) const [code, setCode] = useState('') const [hasPreview, setHasPreview] = useState(false) const [errors, setErrors] = useState<{ title?: string; code?: string }>({}) @@ -47,7 +48,7 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn } else { setTitle('') setDescription('') - setLanguage('JavaScript') + setLanguage(appConfig.defaultLanguage) setCode('') setHasPreview(false) } @@ -58,10 +59,10 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn const newErrors: { title?: string; code?: string } = {} if (!title.trim()) { - newErrors.title = 'Title is required' + newErrors.title = strings.snippetDialog.fields.title.errorMessage } if (!code.trim()) { - newErrors.code = 'Code is required' + newErrors.code = strings.snippetDialog.fields.code.errorMessage } if (Object.keys(newErrors).length > 0) { @@ -79,7 +80,7 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn setTitle('') setDescription('') - setLanguage('JavaScript') + setLanguage(appConfig.defaultLanguage) setCode('') setHasPreview(false) setErrors({}) @@ -91,21 +92,23 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn - {editingSnippet ? 'Edit Snippet' : 'Create New Snippet'} + {editingSnippet ? strings.snippetDialog.edit.title : strings.snippetDialog.create.title} {editingSnippet - ? 'Update your code snippet details below.' - : 'Save a reusable code snippet for quick access later.'} + ? strings.snippetDialog.edit.description + : strings.snippetDialog.create.description}
- + setTitle(e.target.value)} className={errors.title ? 'border-destructive ring-destructive' : ''} @@ -116,7 +119,7 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
- +
- {['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language) && ( + {appConfig.previewEnabledLanguages.includes(language) && (
- Enable split-screen preview for this snippet + {strings.snippetDialog.fields.preview.label}
)}
- +