mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Generated by Spark: Extract strings and things out into json to avoid hardcoding into tsx files.
This commit is contained in:
95
CONFIGURATION.md
Normal file
95
CONFIGURATION.md
Normal file
@@ -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
|
||||
<h1>{strings.app.header.title}</h1>
|
||||
<p>{strings.emptyState.description}</p>
|
||||
```
|
||||
|
||||
### `/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
|
||||
21
src/App.tsx
21
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<Snippet[]>('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() {
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">
|
||||
Code Snippets
|
||||
{strings.app.header.title}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{currentSnippets.length} {currentSnippets.length === 1 ? 'snippet' : 'snippets'} saved
|
||||
{currentSnippets.length} {currentSnippets.length === 1 ? strings.app.header.snippetCount.singular : strings.app.header.snippetCount.plural} saved
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,7 +113,7 @@ function App() {
|
||||
className="gap-2 w-full sm:w-auto"
|
||||
>
|
||||
<Plus weight="bold" />
|
||||
New Snippet
|
||||
{strings.app.header.newSnippetButton}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -121,7 +122,7 @@ function App() {
|
||||
<div className="relative">
|
||||
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search snippets by title, language, or content..."
|
||||
placeholder={strings.app.search.placeholder}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 h-11"
|
||||
@@ -138,9 +139,9 @@ function App() {
|
||||
) : filteredSnippets.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<MagnifyingGlass className="h-16 w-16 text-muted-foreground/50 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold mb-2">No snippets found</h3>
|
||||
<h3 className="text-xl font-semibold mb-2">{strings.noResults.title}</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Try adjusting your search query
|
||||
{strings.noResults.description}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-3xl">
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertTriangleIcon />
|
||||
<AlertTitle>This spark has encountered a runtime error</AlertTitle>
|
||||
onClick={handl
|
||||
>
|
||||
<>
|
||||
|
||||
|
||||
<CopyIcon className="h-3.5 w-3.5" />
|
||||
</>
|
||||
</Button>
|
||||
|
||||
{error.message}
|
||||
|
||||
<CollapsibleTrigger as
|
||||
variant="ghost"
|
||||
|
||||
{isStackO
|
||||
|
||||
</>
|
||||
<>
|
||||
|
||||
)}
|
||||
</Coll
|
||||
<div className="mt-4">
|
||||
{error.s
|
||||
</div
|
||||
</Coll
|
||||
|
||||
<AIErr
|
||||
|
||||
variant="outline"
|
||||
>
|
||||
Tr
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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 (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-3xl">
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertTriangleIcon />
|
||||
<AlertTitle>This spark has encountered a runtime error</AlertTitle>
|
||||
<AlertDescription className="mt-3 space-y-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<code className="text-sm bg-destructive/20 px-2 py-1 rounded flex-1 break-all">
|
||||
{error.message}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleCopy}
|
||||
className="shrink-0"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckIcon className="h-3.5 w-3.5" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CopyIcon className="h-3.5 w-3.5" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Collapsible open={isStackOpen} onOpenChange={setIsStackOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{isStackOpen ? (
|
||||
<>
|
||||
Hide Stack Trace <ChevronUpIcon className="h-4 w-4 ml-2" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Show Stack Trace <ChevronDownIcon className="h-4 w-4 ml-2" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-4">
|
||||
<pre className="text-xs bg-destructive/10 p-3 rounded overflow-auto max-h-60">
|
||||
{error.stack || 'No stack trace available'}
|
||||
</pre>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<AIErrorHelper error={error} />
|
||||
|
||||
<Button
|
||||
onClick={() => window.location.reload()}
|
||||
className="w-full mt-6"
|
||||
variant="outline"
|
||||
>
|
||||
<RefreshCwIcon className="h-4 w-4 mr-2" />
|
||||
Try Reloading
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
<div className="rounded-full bg-accent/10 p-6 mb-6">
|
||||
<Code className="h-16 w-16 text-accent" weight="duotone" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold mb-2">No snippets yet</h3>
|
||||
<h3 className="text-2xl font-semibold mb-2">{strings.emptyState.title}</h3>
|
||||
<p className="text-muted-foreground mb-8 max-w-sm">
|
||||
Start building your code snippet library. Save reusable code for quick access anytime.
|
||||
{strings.emptyState.description}
|
||||
</p>
|
||||
<Button onClick={onCreateClick} size="lg" className="gap-2">
|
||||
<Code className="h-5 w-5" weight="bold" />
|
||||
Create Your First Snippet
|
||||
{strings.emptyState.buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<Card className="p-6">
|
||||
<p className="text-muted-foreground">Error loading snippet</p>
|
||||
<p className="text-muted-foreground">{strings.snippetCard.errorMessage}</p>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -101,7 +102,7 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp
|
||||
</pre>
|
||||
{snippetData.isTruncated && (
|
||||
<p className="text-xs text-accent mt-2">
|
||||
Click to view full code...
|
||||
{strings.snippetCard.viewFullCode}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -115,7 +116,7 @@ export function SnippetCard({ snippet, onEdit, onDelete, onCopy, onView }: Snipp
|
||||
className="gap-2"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
View
|
||||
{strings.snippetCard.viewButton}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -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}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
{isCopied ? 'Copied!' : 'Copy'}
|
||||
{isCopied ? strings.snippetCard.copiedButton : strings.snippetCard.copyButton}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleEdit}
|
||||
aria-label="Edit snippet"
|
||||
aria-label={strings.snippetCard.ariaLabels.edit}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -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}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -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
|
||||
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">
|
||||
{editingSnippet ? 'Edit Snippet' : 'Create New Snippet'}
|
||||
{editingSnippet ? strings.snippetDialog.edit.title : strings.snippetDialog.create.title}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editingSnippet
|
||||
? 'Update your code snippet details below.'
|
||||
: 'Save a reusable code snippet for quick access later.'}
|
||||
? strings.snippetDialog.edit.description
|
||||
: strings.snippetDialog.create.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4 overflow-y-auto flex-1">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Title *</Label>
|
||||
<Label htmlFor="title">
|
||||
{strings.snippetDialog.fields.title.label}{strings.snippetDialog.fields.title.required ? ' *' : ''}
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="e.g., React useState Hook"
|
||||
placeholder={strings.snippetDialog.fields.title.placeholder}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className={errors.title ? 'border-destructive ring-destructive' : ''}
|
||||
@@ -116,7 +119,7 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">Language</Label>
|
||||
<Label htmlFor="language">{strings.snippetDialog.fields.language.label}</Label>
|
||||
<Select value={language} onValueChange={setLanguage}>
|
||||
<SelectTrigger id="language">
|
||||
<SelectValue />
|
||||
@@ -131,7 +134,7 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language) && (
|
||||
{appConfig.previewEnabledLanguages.includes(language) && (
|
||||
<div className="flex items-center space-x-2 py-2">
|
||||
<Checkbox
|
||||
id="hasPreview"
|
||||
@@ -142,16 +145,16 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
htmlFor="hasPreview"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
Enable split-screen preview for this snippet
|
||||
{strings.snippetDialog.fields.preview.label}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Label htmlFor="description">{strings.snippetDialog.fields.description.label}</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Optional description or notes..."
|
||||
placeholder={strings.snippetDialog.fields.description.placeholder}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={2}
|
||||
@@ -159,7 +162,9 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="code">Code *</Label>
|
||||
<Label htmlFor="code">
|
||||
{strings.snippetDialog.fields.code.label}{strings.snippetDialog.fields.code.required ? ' *' : ''}
|
||||
</Label>
|
||||
<div className={`rounded-md border overflow-hidden ${errors.code ? 'border-destructive ring-2 ring-destructive/20' : 'border-border'}`}>
|
||||
<MonacoEditor
|
||||
value={code}
|
||||
@@ -176,10 +181,10 @@ export function SnippetDialog({ open, onOpenChange, onSave, editingSnippet }: Sn
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
{strings.snippetDialog.buttons.cancel}
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
{editingSnippet ? 'Update' : 'Create'} Snippet
|
||||
{editingSnippet ? strings.snippetDialog.buttons.update : strings.snippetDialog.buttons.create} Snippet
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { MonacoEditor } from '@/components/MonacoEditor'
|
||||
import { ReactPreview } from '@/components/ReactPreview'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
import { strings, appConfig } from '@/lib/config'
|
||||
|
||||
interface SnippetViewerProps {
|
||||
snippet: Snippet | null
|
||||
@@ -30,7 +31,7 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
const handleCopy = () => {
|
||||
onCopy(snippet.code)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => setIsCopied(false), 2000)
|
||||
setTimeout(() => setIsCopied(false), appConfig.copiedTimeout)
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
@@ -38,7 +39,7 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
onEdit(snippet)
|
||||
}
|
||||
|
||||
const canPreview = snippet.hasPreview && ['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(snippet.language)
|
||||
const canPreview = snippet.hasPreview && appConfig.previewEnabledLanguages.includes(snippet.language)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -66,7 +67,7 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Last updated: {new Date(snippet.updatedAt).toLocaleString()}
|
||||
{strings.snippetViewer.lastUpdated}: {new Date(snippet.updatedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
@@ -78,7 +79,7 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
className="gap-2"
|
||||
>
|
||||
<SplitVertical className="h-4 w-4" />
|
||||
{showPreview ? 'Hide' : 'Show'} Preview
|
||||
{showPreview ? strings.snippetViewer.buttons.hidePreview : strings.snippetViewer.buttons.showPreview}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -90,12 +91,12 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
{isCopied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" weight="bold" />
|
||||
Copied
|
||||
{strings.snippetViewer.buttons.copied}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy
|
||||
{strings.snippetViewer.buttons.copy}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -106,7 +107,7 @@ export function SnippetViewer({ snippet, open, onOpenChange, onEdit, onCopy }: S
|
||||
className="gap-2"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
Edit
|
||||
{strings.snippetViewer.buttons.edit}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
124
src/config/app-config.json
Normal file
124
src/config/app-config.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"languages": [
|
||||
"JavaScript",
|
||||
"TypeScript",
|
||||
"JSX",
|
||||
"TSX",
|
||||
"Python",
|
||||
"Java",
|
||||
"C++",
|
||||
"C#",
|
||||
"Ruby",
|
||||
"Go",
|
||||
"Rust",
|
||||
"PHP",
|
||||
"Swift",
|
||||
"Kotlin",
|
||||
"HTML",
|
||||
"CSS",
|
||||
"SQL",
|
||||
"Bash",
|
||||
"Other"
|
||||
],
|
||||
"languageColors": {
|
||||
"JavaScript": {
|
||||
"bg": "bg-yellow-500/20",
|
||||
"text": "text-yellow-300",
|
||||
"border": "border-yellow-500/30"
|
||||
},
|
||||
"TypeScript": {
|
||||
"bg": "bg-blue-500/20",
|
||||
"text": "text-blue-300",
|
||||
"border": "border-blue-500/30"
|
||||
},
|
||||
"JSX": {
|
||||
"bg": "bg-cyan-500/20",
|
||||
"text": "text-cyan-300",
|
||||
"border": "border-cyan-500/30"
|
||||
},
|
||||
"TSX": {
|
||||
"bg": "bg-sky-500/20",
|
||||
"text": "text-sky-300",
|
||||
"border": "border-sky-500/30"
|
||||
},
|
||||
"Python": {
|
||||
"bg": "bg-blue-400/20",
|
||||
"text": "text-blue-200",
|
||||
"border": "border-blue-400/30"
|
||||
},
|
||||
"Java": {
|
||||
"bg": "bg-red-500/20",
|
||||
"text": "text-red-300",
|
||||
"border": "border-red-500/30"
|
||||
},
|
||||
"C++": {
|
||||
"bg": "bg-pink-500/20",
|
||||
"text": "text-pink-300",
|
||||
"border": "border-pink-500/30"
|
||||
},
|
||||
"C#": {
|
||||
"bg": "bg-purple-500/20",
|
||||
"text": "text-purple-300",
|
||||
"border": "border-purple-500/30"
|
||||
},
|
||||
"Ruby": {
|
||||
"bg": "bg-red-600/20",
|
||||
"text": "text-red-300",
|
||||
"border": "border-red-600/30"
|
||||
},
|
||||
"Go": {
|
||||
"bg": "bg-cyan-500/20",
|
||||
"text": "text-cyan-300",
|
||||
"border": "border-cyan-500/30"
|
||||
},
|
||||
"Rust": {
|
||||
"bg": "bg-orange-600/20",
|
||||
"text": "text-orange-300",
|
||||
"border": "border-orange-600/30"
|
||||
},
|
||||
"PHP": {
|
||||
"bg": "bg-indigo-500/20",
|
||||
"text": "text-indigo-300",
|
||||
"border": "border-indigo-500/30"
|
||||
},
|
||||
"Swift": {
|
||||
"bg": "bg-orange-500/20",
|
||||
"text": "text-orange-300",
|
||||
"border": "border-orange-500/30"
|
||||
},
|
||||
"Kotlin": {
|
||||
"bg": "bg-purple-600/20",
|
||||
"text": "text-purple-300",
|
||||
"border": "border-purple-600/30"
|
||||
},
|
||||
"HTML": {
|
||||
"bg": "bg-orange-400/20",
|
||||
"text": "text-orange-300",
|
||||
"border": "border-orange-400/30"
|
||||
},
|
||||
"CSS": {
|
||||
"bg": "bg-blue-600/20",
|
||||
"text": "text-blue-300",
|
||||
"border": "border-blue-600/30"
|
||||
},
|
||||
"SQL": {
|
||||
"bg": "bg-teal-500/20",
|
||||
"text": "text-teal-300",
|
||||
"border": "border-teal-500/30"
|
||||
},
|
||||
"Bash": {
|
||||
"bg": "bg-green-500/20",
|
||||
"text": "text-green-300",
|
||||
"border": "border-green-500/30"
|
||||
},
|
||||
"Other": {
|
||||
"bg": "bg-gray-500/20",
|
||||
"text": "text-gray-300",
|
||||
"border": "border-gray-500/30"
|
||||
}
|
||||
},
|
||||
"previewEnabledLanguages": ["JSX", "TSX", "JavaScript", "TypeScript"],
|
||||
"defaultLanguage": "JavaScript",
|
||||
"codePreviewMaxLength": 150,
|
||||
"copiedTimeout": 2000
|
||||
}
|
||||
93
src/config/strings.json
Normal file
93
src/config/strings.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "Code Snippets",
|
||||
"header": {
|
||||
"title": "Code Snippets",
|
||||
"snippetCount": {
|
||||
"singular": "snippet",
|
||||
"plural": "snippets"
|
||||
},
|
||||
"newSnippetButton": "New Snippet"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search snippets by title, language, or content..."
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "No snippets yet",
|
||||
"description": "Start building your code snippet library. Save reusable code for quick access anytime.",
|
||||
"buttonText": "Create Your First Snippet"
|
||||
},
|
||||
"noResults": {
|
||||
"title": "No snippets found",
|
||||
"description": "Try adjusting your search query"
|
||||
},
|
||||
"snippetCard": {
|
||||
"viewButton": "View",
|
||||
"copyButton": "Copy",
|
||||
"copiedButton": "Copied!",
|
||||
"viewFullCode": "Click to view full code...",
|
||||
"errorMessage": "Error loading snippet",
|
||||
"ariaLabels": {
|
||||
"copy": "Copy code",
|
||||
"edit": "Edit snippet",
|
||||
"delete": "Delete snippet"
|
||||
}
|
||||
},
|
||||
"snippetDialog": {
|
||||
"create": {
|
||||
"title": "Create New Snippet",
|
||||
"description": "Save a reusable code snippet for quick access later.",
|
||||
"submitButton": "Create Snippet"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit Snippet",
|
||||
"description": "Update your code snippet details below.",
|
||||
"submitButton": "Update Snippet"
|
||||
},
|
||||
"fields": {
|
||||
"title": {
|
||||
"label": "Title",
|
||||
"placeholder": "e.g., React useState Hook",
|
||||
"required": true,
|
||||
"errorMessage": "Title is required"
|
||||
},
|
||||
"language": {
|
||||
"label": "Language"
|
||||
},
|
||||
"description": {
|
||||
"label": "Description",
|
||||
"placeholder": "Optional description or notes..."
|
||||
},
|
||||
"code": {
|
||||
"label": "Code",
|
||||
"required": true,
|
||||
"errorMessage": "Code is required"
|
||||
},
|
||||
"preview": {
|
||||
"label": "Enable split-screen preview for this snippet"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"update": "Update"
|
||||
}
|
||||
},
|
||||
"snippetViewer": {
|
||||
"lastUpdated": "Last updated",
|
||||
"buttons": {
|
||||
"showPreview": "Show Preview",
|
||||
"hidePreview": "Hide Preview",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"edit": "Edit"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
"snippetCreated": "Snippet created successfully",
|
||||
"snippetUpdated": "Snippet updated successfully",
|
||||
"snippetDeleted": "Snippet deleted",
|
||||
"codeCopied": "Code copied to clipboard"
|
||||
}
|
||||
}
|
||||
24
src/lib/config.ts
Normal file
24
src/lib/config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import stringsData from '@/config/strings.json'
|
||||
import appConfigData from '@/config/app-config.json'
|
||||
|
||||
export const strings = stringsData
|
||||
export const appConfig = appConfigData
|
||||
|
||||
export function getLanguageColor(language: string): string {
|
||||
const colors = appConfig.languageColors[language as keyof typeof appConfig.languageColors]
|
||||
if (colors) {
|
||||
return `${colors.bg} ${colors.text} ${colors.border}`
|
||||
}
|
||||
const defaultColors = appConfig.languageColors.Other
|
||||
return `${defaultColors.bg} ${defaultColors.text} ${defaultColors.border}`
|
||||
}
|
||||
|
||||
export const LANGUAGES = appConfig.languages
|
||||
|
||||
export const LANGUAGE_COLORS: Record<string, string> = Object.entries(appConfig.languageColors).reduce(
|
||||
(acc, [key, value]) => {
|
||||
acc[key] = `${value.bg} ${value.text} ${value.border}`
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, string>
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
import { LANGUAGES, LANGUAGE_COLORS } from './config'
|
||||
|
||||
export interface Snippet {
|
||||
id: string
|
||||
title: string
|
||||
@@ -9,48 +11,6 @@ export interface Snippet {
|
||||
hasPreview?: boolean
|
||||
}
|
||||
|
||||
export const LANGUAGES = [
|
||||
'JavaScript',
|
||||
'TypeScript',
|
||||
'JSX',
|
||||
'TSX',
|
||||
'Python',
|
||||
'Java',
|
||||
'C++',
|
||||
'C#',
|
||||
'Ruby',
|
||||
'Go',
|
||||
'Rust',
|
||||
'PHP',
|
||||
'Swift',
|
||||
'Kotlin',
|
||||
'HTML',
|
||||
'CSS',
|
||||
'SQL',
|
||||
'Bash',
|
||||
'Other'
|
||||
] as const
|
||||
|
||||
export type Language = typeof LANGUAGES[number]
|
||||
|
||||
export const LANGUAGE_COLORS: Record<string, string> = {
|
||||
'JavaScript': 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30',
|
||||
'TypeScript': 'bg-blue-500/20 text-blue-300 border-blue-500/30',
|
||||
'JSX': 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30',
|
||||
'TSX': 'bg-sky-500/20 text-sky-300 border-sky-500/30',
|
||||
'Python': 'bg-blue-400/20 text-blue-200 border-blue-400/30',
|
||||
'Java': 'bg-red-500/20 text-red-300 border-red-500/30',
|
||||
'C++': 'bg-pink-500/20 text-pink-300 border-pink-500/30',
|
||||
'C#': 'bg-purple-500/20 text-purple-300 border-purple-500/30',
|
||||
'Ruby': 'bg-red-600/20 text-red-300 border-red-600/30',
|
||||
'Go': 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30',
|
||||
'Rust': 'bg-orange-600/20 text-orange-300 border-orange-600/30',
|
||||
'PHP': 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30',
|
||||
'Swift': 'bg-orange-500/20 text-orange-300 border-orange-500/30',
|
||||
'Kotlin': 'bg-purple-600/20 text-purple-300 border-purple-600/30',
|
||||
'HTML': 'bg-orange-400/20 text-orange-300 border-orange-400/30',
|
||||
'CSS': 'bg-blue-600/20 text-blue-300 border-blue-600/30',
|
||||
'SQL': 'bg-teal-500/20 text-teal-300 border-teal-500/30',
|
||||
'Bash': 'bg-green-500/20 text-green-300 border-green-500/30',
|
||||
'Other': 'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
}
|
||||
export { LANGUAGES, LANGUAGE_COLORS }
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
Reference in New Issue
Block a user