Skip to content

Commit 5ae8318

Browse files
committed
Mardown functionality
1 parent 73fd11f commit 5ae8318

13 files changed

Lines changed: 2386 additions & 111 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dist-ssr
2424
*.sw?
2525
.context/
2626
GEMINI.md
27+
.playwright-mcp/
2728

2829
# Snyk Security Extension - AI Rules (auto-generated)
2930
.github/instructions/snyk_rules.instructions.md

README.md

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ This tool is ideal for developers, writers, marketers, and anyone who frequently
5555

5656
### Features
5757

58-
- **Prompt Management:** Create, edit, and delete prompts with rich content.
58+
- **Prompt Management:** Create, edit, and delete prompts with a rich markdown editor.
5959
- **Category Management:** Organize prompts into custom categories for structured access.
6060
- **Tagging System:** Apply multiple tags to prompts for flexible cross-categorization and discoverability.
6161
- **Favorites:** Mark important prompts as favorites for quick and easy access.
@@ -71,9 +71,9 @@ To get PromptVault up and running on your local machine, follow these steps.
7171

7272
### Prerequisites
7373

74-
- Git (for cloning the repository)
75-
- Node.js (LTS version recommended)
76-
- npm (Node Package Manager, usually installed with Node.js) or Yarn
74+
- **Git** (for cloning the repository)
75+
- **Node.js** (LTS version, 18.x or higher recommended)
76+
- **npm** (Node Package Manager, usually installed with Node.js) or Yarn
7777

7878
### Setup Instructions
7979

@@ -100,13 +100,33 @@ To get PromptVault up and running on your local machine, follow these steps.
100100
```sh
101101
npm run dev
102102
```
103-
The application will typically open in your default browser at `http://localhost:3000`.
103+
The application will typically open in your default browser at `http://localhost:5173`.
104+
105+
5. Build for production (optional):
106+
107+
```sh
108+
npm run build
109+
```
110+
This command compiles the application into a production-ready bundle, located in the `dist` directory.
111+
112+
6. Preview production build (optional):
113+
114+
```sh
115+
npm run preview
116+
```
117+
This will serve the production build locally.
118+
119+
7. Deploy to GitHub Pages (optional, requires `gh-pages` configuration):
120+
121+
```sh
122+
npm run deploy
123+
```
104124

105125
## ⚙️ Usage
106126

107127
PromptVault is designed for intuitive use. Here's a basic guide to get started:
108128
109-
1. **Add New Prompts:** Click the "New" button (usually a plus icon) to open the editor. Enter your prompt's title, content, select a category, and add relevant tags.
129+
1. **Add New Prompts:** Click the "New" button (usually a plus icon) to open the editor. Enter your prompt's title, content (which supports markdown), select a category, and add relevant tags.
110130
2. **Organize with Categories & Tags:** Use the sidebar to create and manage categories. Assign categories and tags to your prompts to keep them organized. Click on tags in the sidebar or on prompt cards to filter by them.
111131
3. **Search and Filter:** Use the search bar in the header to find prompts by keywords. Utilize the filter options (category, favorites, tags) to narrow down your search.
112132
4. **Edit and Delete:** Hover over a prompt card (or click for list view) to reveal edit (pencil icon) and delete (trash icon) options.
@@ -120,6 +140,8 @@ PromptVault is designed for intuitive use. Here's a basic guide to get started:
120140
- **Tailwind CSS:** A utility-first CSS framework for rapidly building custom designs.
121141
- **Lucide React:** A collection of beautiful open-source icons.
122142
- **uuid:** A library for generating unique identifiers.
143+
- **react-markdown:** A React component to render Markdown.
144+
- **Local Storage:** Browser API used for client-side data persistence.
123145

124146
## 🔒 Security Notes
125147

@@ -165,4 +187,4 @@ We welcome contributions from the community! If you have suggestions for improve
165187

166188
## ⚠️ Disclaimer
167189

168-
PromptVault is a client-side application designed for personal use. All data is stored locally in your browser's local storage and is not transmitted to external servers. Users are solely responsible for managing their local data, including implementing their own backup strategies for important prompts. This tool is provided "as is," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
190+
PromptVault is a client-side application designed for personal use. All data is stored locally in your browser's local storage and is not transmitted to external servers. Users are solely responsible for managing their local data, including implementing their own backup strategies for important prompts. This tool is provided "as is," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.

components/CombinedPromptModal.tsx

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { X, Copy, Check, Edit, Save } from 'lucide-react';
33
import { Prompt, Category } from '../types';
44
import { CustomSelect } from './CustomSelect';
55
import { TagBadge } from './TagBadge';
6+
import { MarkdownEditor } from './MarkdownEditor';
7+
import { MarkdownRenderer } from './MarkdownRenderer';
8+
import { hasMarkdownSyntax } from '../utils/markdownDetection';
69

710
interface CombinedPromptModalProps {
811
isOpen: boolean;
@@ -109,6 +112,9 @@ export function CombinedPromptModal({
109112
});
110113
};
111114

115+
// Check if content contains markdown syntax
116+
const containsMarkdown = prompt ? hasMarkdownSyntax(prompt.content) : false;
117+
112118
return (
113119
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
114120
<div className="bg-black-100 border border-black-300 w-full max-w-4xl rounded-2xl shadow-2xl flex flex-col max-h-[90vh]">
@@ -182,17 +188,11 @@ export function CombinedPromptModal({
182188
/>
183189
</div>
184190

185-
<div className="space-y-2">
186-
<div className="flex items-center justify-between">
187-
<label className="text-sm font-medium text-zinc-300">Prompt Content</label>
188-
</div>
189-
<textarea
190-
value={content}
191-
onChange={(e) => setContent(e.target.value)}
192-
placeholder="Enter your prompt text here..."
193-
className="w-full h-64 bg-black-200 border border-black-300 rounded-lg p-4 text-white font-mono text-sm focus:ring-2 focus:ring-accent focus:border-transparent outline-none transition-all resize-none leading-relaxed"
194-
/>
195-
</div>
191+
<MarkdownEditor
192+
value={content}
193+
onChange={setContent}
194+
placeholder="Enter your prompt text here... (Markdown formatting supported)"
195+
/>
196196

197197
<div className="space-y-2">
198198
<label className="text-sm font-medium text-zinc-300">Tags (comma separated)</label>
@@ -238,11 +238,19 @@ export function CombinedPromptModal({
238238
{prompt && (
239239
<>
240240
<div className="bg-black-200/50 rounded-lg p-6 border border-black-300">
241-
<div className="text-zinc-200 whitespace-pre-wrap font-mono text-sm leading-relaxed">
242-
{prompt.isTemplate
243-
? renderContentWithHighlightedVariables(prompt.content)
244-
: prompt.content}
245-
</div>
241+
{containsMarkdown && !prompt.isTemplate ? (
242+
<div className="text-zinc-200 leading-relaxed">
243+
<MarkdownRenderer content={prompt.content} />
244+
</div>
245+
) : prompt.isTemplate ? (
246+
<div className="text-zinc-200 whitespace-pre-wrap font-mono text-sm leading-relaxed">
247+
{renderContentWithHighlightedVariables(prompt.content)}
248+
</div>
249+
) : (
250+
<div className="text-zinc-200 whitespace-pre-wrap font-mono text-sm leading-relaxed">
251+
{prompt.content}
252+
</div>
253+
)}
246254
</div>
247255

248256
{prompt.tags.length > 0 && (
@@ -327,4 +335,4 @@ export function CombinedPromptModal({
327335
</div>
328336
</div>
329337
);
330-
}
338+
}

components/EditorModal.tsx

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
22
import { Prompt, Category } from '../types';
33
import { X, Save } from 'lucide-react';
44
import { CustomSelect } from './CustomSelect';
5+
import { MarkdownEditor } from './MarkdownEditor';
56

67
interface EditorModalProps {
78
isOpen: boolean;
@@ -110,18 +111,11 @@ const EditorModal: React.FC<EditorModalProps> = ({
110111
</div>
111112
</div>
112113

113-
<div className="space-y-2">
114-
<div className="flex items-center justify-between">
115-
<label className="text-sm font-medium text-zinc-300">Prompt Content</label>
116-
</div>
117-
<textarea
118-
required
119-
value={content}
120-
onChange={(e) => setContent(e.target.value)}
121-
placeholder="Enter your prompt text here..."
122-
className="w-full h-64 bg-black-200 border border-black-300 rounded-lg p-4 text-white font-mono text-sm focus:ring-2 focus:ring-accent focus:border-transparent outline-none transition-all resize-none leading-relaxed"
123-
/>
124-
</div>
114+
<MarkdownEditor
115+
value={content}
116+
onChange={setContent}
117+
placeholder="Enter your prompt text here... (Markdown formatting supported)"
118+
/>
125119

126120
<div className="space-y-2">
127121
<label className="text-sm font-medium text-zinc-300">Tags (comma separated)</label>
@@ -194,4 +188,4 @@ const TagIcon = ({ className }: { className?: string }) => (
194188
</svg>
195189
);
196190

197-
export default EditorModal;
191+
export default EditorModal;

components/ExpandedPromptModal.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { useState } from 'react';
22
import { X, Copy, Check, Edit } from 'lucide-react';
33
import { Prompt, Category } from '../types';
44
import { TagBadge } from './TagBadge';
5+
import { MarkdownRenderer } from './MarkdownRenderer';
6+
import { hasMarkdownSyntax } from '../utils/markdownDetection';
57

68
interface ExpandedPromptModalProps {
79
isOpen: boolean;
@@ -59,6 +61,9 @@ export function ExpandedPromptModal({
5961
onClose();
6062
};
6163

64+
// Check if content contains markdown syntax
65+
const containsMarkdown = hasMarkdownSyntax(prompt.content);
66+
6267
return (
6368
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
6469
<div className="bg-black-100 border border-black-300 w-full max-w-4xl rounded-2xl shadow-2xl flex flex-col max-h-[90vh]">
@@ -94,11 +99,19 @@ export function ExpandedPromptModal({
9499
{/* Content */}
95100
<div className="flex-1 overflow-y-auto p-6 space-y-6">
96101
<div className="bg-black-200/50 rounded-lg p-6 border border-black-300">
97-
<div className="text-zinc-200 whitespace-pre-wrap font-mono text-sm leading-relaxed">
98-
{prompt.isTemplate
99-
? renderContentWithHighlightedVariables(prompt.content)
100-
: prompt.content}
101-
</div>
102+
{containsMarkdown && !prompt.isTemplate ? (
103+
<div className="text-zinc-200 leading-relaxed">
104+
<MarkdownRenderer content={prompt.content} />
105+
</div>
106+
) : prompt.isTemplate ? (
107+
<div className="text-zinc-200 whitespace-pre-wrap font-mono text-sm leading-relaxed">
108+
{renderContentWithHighlightedVariables(prompt.content)}
109+
</div>
110+
) : (
111+
<div className="text-zinc-200 whitespace-pre-wrap font-mono text-sm leading-relaxed">
112+
{prompt.content}
113+
</div>
114+
)}
102115
</div>
103116

104117
{/* Tags section */}
@@ -157,4 +170,4 @@ export function ExpandedPromptModal({
157170
</div>
158171
</div>
159172
);
160-
}
173+
}

components/MarkdownEditor.tsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React, { useState, useRef } from 'react';
2+
import { Eye, Edit2 } from 'lucide-react';
3+
import { MarkdownRenderer } from './MarkdownRenderer';
4+
import '../styles/markdown.css';
5+
6+
interface MarkdownEditorProps {
7+
value: string;
8+
onChange: (value: string) => void;
9+
placeholder?: string;
10+
}
11+
12+
type EditorLayout = 'split' | 'edit' | 'preview';
13+
14+
export const MarkdownEditor: React.FC<MarkdownEditorProps> = ({
15+
value,
16+
onChange,
17+
placeholder = 'Enter your content here...',
18+
}) => {
19+
const [layout, setLayout] = useState<EditorLayout>('split');
20+
const textareaRef = useRef<HTMLTextAreaElement>(null);
21+
const previewRef = useRef<HTMLDivElement>(null);
22+
const isSyncingRef = useRef(false);
23+
24+
// Handle synchronized scrolling
25+
const handleEditScroll = () => {
26+
if (!textareaRef.current || !previewRef.current || layout !== 'split') return;
27+
28+
isSyncingRef.current = true;
29+
const scrollPercentage =
30+
textareaRef.current.scrollTop /
31+
(textareaRef.current.scrollHeight - textareaRef.current.clientHeight);
32+
33+
previewRef.current.scrollTop =
34+
scrollPercentage * (previewRef.current.scrollHeight - previewRef.current.clientHeight);
35+
36+
setTimeout(() => {
37+
isSyncingRef.current = false;
38+
}, 0);
39+
};
40+
41+
const handlePreviewScroll = () => {
42+
if (!textareaRef.current || !previewRef.current || layout !== 'split') return;
43+
44+
if (isSyncingRef.current) return;
45+
46+
isSyncingRef.current = true;
47+
const scrollPercentage =
48+
previewRef.current.scrollHeight > previewRef.current.clientHeight
49+
? previewRef.current.scrollTop /
50+
(previewRef.current.scrollHeight - previewRef.current.clientHeight)
51+
: 0;
52+
53+
textareaRef.current.scrollTop =
54+
scrollPercentage * (textareaRef.current.scrollHeight - textareaRef.current.clientHeight);
55+
56+
setTimeout(() => {
57+
isSyncingRef.current = false;
58+
}, 0);
59+
};
60+
61+
const layoutButtons = [
62+
{ value: 'edit' as const, label: 'Edit', icon: Edit2 },
63+
{ value: 'split' as const, label: 'Split', icon: null },
64+
{ value: 'preview' as const, label: 'Preview', icon: Eye },
65+
];
66+
67+
return (
68+
<div className="space-y-2">
69+
<div className="flex items-center justify-between">
70+
<label className="text-sm font-medium text-zinc-300">Prompt Content</label>
71+
<div className="flex items-center gap-1 p-0.5 bg-black-200 border border-black-300 rounded-lg">
72+
{layoutButtons.map((btn) => (
73+
<button
74+
key={btn.value}
75+
type="button"
76+
onClick={() => setLayout(btn.value)}
77+
className={`flex items-center gap-1 px-3 py-1.5 rounded transition-all text-xs font-medium ${
78+
layout === btn.value
79+
? 'bg-accent/20 text-accent border border-accent/50'
80+
: 'text-zinc-400 hover:text-zinc-300'
81+
}`}
82+
title={btn.label}
83+
>
84+
{btn.icon && <btn.icon className="w-3.5 h-3.5" />}
85+
{btn.label}
86+
</button>
87+
))}
88+
</div>
89+
</div>
90+
91+
<div className="relative bg-black-200 border border-black-300 rounded-lg overflow-hidden">
92+
{layout === 'edit' && (
93+
<textarea
94+
ref={textareaRef}
95+
value={value}
96+
onChange={(e) => onChange(e.target.value)}
97+
placeholder={placeholder}
98+
className="w-full h-64 bg-black-200 p-4 text-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:ring-inset transition-all resize-none leading-relaxed"
99+
/>
100+
)}
101+
102+
{layout === 'preview' && (
103+
<div className="w-full min-h-64 bg-black-200/50 p-4 overflow-y-auto">
104+
<MarkdownRenderer content={value} className="text-sm" />
105+
</div>
106+
)}
107+
108+
{layout === 'split' && (
109+
<div className="flex h-96 divide-x divide-black-300">
110+
{/* Edit pane */}
111+
<div className="flex-1 flex flex-col min-w-0">
112+
<textarea
113+
ref={textareaRef}
114+
value={value}
115+
onChange={(e) => onChange(e.target.value)}
116+
onScroll={handleEditScroll}
117+
placeholder={placeholder}
118+
className="flex-1 bg-black-200 p-4 text-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:ring-inset transition-all resize-none leading-relaxed overflow-y-auto"
119+
/>
120+
</div>
121+
122+
{/* Preview pane */}
123+
<div
124+
ref={previewRef}
125+
onScroll={handlePreviewScroll}
126+
className="flex-1 overflow-y-auto bg-black-300/30 p-4"
127+
>
128+
<MarkdownRenderer content={value} className="text-sm" />
129+
</div>
130+
</div>
131+
)}
132+
</div>
133+
134+
<p className="text-xs text-zinc-500 px-1">
135+
{layout === 'edit' && 'Edit mode: Markdown formatting is supported.'}
136+
{layout === 'preview' && 'Preview mode: See how your markdown will look.'}
137+
{layout === 'split' && 'Split mode: Edit on left, preview on right. Scrolling is synchronized.'}
138+
</p>
139+
</div>
140+
);
141+
};

0 commit comments

Comments
 (0)