Skip to content

Commit a433868

Browse files
init changelog
1 parent fbfa4cc commit a433868

File tree

8 files changed

+2546
-207
lines changed

8 files changed

+2546
-207
lines changed

frontend/next.config.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
33
output: "standalone",
4+
webpack: (config, { isServer }) => {
5+
if (!isServer) {
6+
config.resolve.fallback = {
7+
fs: false,
8+
net: false,
9+
tls: false,
10+
};
11+
}
12+
return config;
13+
},
414

515
typescript: {
616
// !! WARN !!

frontend/package-lock.json

Lines changed: 2363 additions & 137 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@editorjs/code": "^2.9.2",
13+
"@editorjs/editorjs": "^2.30.6",
14+
"@editorjs/embed": "^2.7.4",
15+
"@editorjs/header": "^2.8.7",
16+
"@editorjs/image": "^2.9.3",
17+
"@editorjs/list": "^1.10.0",
1218
"@hookform/resolvers": "^3.9.0",
1319
"@radix-ui/react-accordion": "^1.2.0",
1420
"@radix-ui/react-alert-dialog": "^1.1.1",
@@ -37,6 +43,8 @@
3743
"@radix-ui/react-toggle": "^1.1.0",
3844
"@radix-ui/react-toggle-group": "^1.1.0",
3945
"@radix-ui/react-tooltip": "^1.1.2",
46+
"@tailwindcss/typography": "^0.5.15",
47+
"@uiw/react-md-editor": "^4.0.4",
4048
"canvas-confetti": "^1.9.3",
4149
"class-variance-authority": "^0.7.0",
4250
"clsx": "^2.1.1",
@@ -56,6 +64,7 @@
5664
"react-dnd-touch-backend": "^16.0.1",
5765
"react-dom": "^18",
5866
"react-hook-form": "^7.52.2",
67+
"react-markdown": "^9.0.1",
5968
"react-resizable-panels": "^2.0.22",
6069
"recharts": "^2.12.7",
6170
"slugify": "^1.6.6",

frontend/src/app/[slug]/changelog/[id]/page.tsx

Lines changed: 152 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,85 @@
11
"use client"
2-
import React, { useState, useEffect } from 'react';
2+
import React, { useState, useEffect, useRef } from 'react';
33
import { Button } from "@/components/ui/button";
44
import { Input } from "@/components/ui/input";
5-
import { Textarea } from "@/components/ui/textarea";
65
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
76
import { Badge } from "@/components/ui/badge";
87
import { X } from "lucide-react";
98
import { useToast } from "@/components/ui/use-toast";
109
import FileUploadButton from '@/components/FileButton';
10+
import { Icons } from '@/components/icons';
11+
import dynamic from 'next/dynamic';
12+
13+
const EditorJS = dynamic(() => import('@editorjs/editorjs'), { ssr: false });
1114

1215
const ChangelogEditor = ({ params }) => {
1316
const { toast } = useToast();
17+
const editorRef = useRef(null);
1418
const [changelogData, setChangelogData] = useState({
1519
title: '',
16-
content: '',
20+
content: null,
1721
coverImage: null,
1822
tags: [],
1923
});
2024
const [isDraft, setIsDraft] = useState(true);
2125
const [newTag, setNewTag] = useState('');
26+
const [uploading, setUploading] = useState(false);
27+
const fileInputRef = useRef(null);
2228

2329
useEffect(() => {
2430
fetchChangelogData();
2531
}, [params.id, params.slug]);
2632

33+
useEffect(() => {
34+
if (typeof window !== 'undefined') {
35+
initEditor();
36+
}
37+
}, []);
38+
39+
const initEditor = async () => {
40+
const EditorJS = (await import('@editorjs/editorjs')).default;
41+
const Header = (await import('@editorjs/header')).default;
42+
const List = (await import('@editorjs/list')).default;
43+
const Embed = (await import('@editorjs/embed')).default;
44+
const Image = (await import('@editorjs/image')).default;
45+
const Code = (await import('@editorjs/code')).default;
46+
47+
if (!editorRef.current) {
48+
const editor = new EditorJS({
49+
holder: 'editorjs',
50+
tools: {
51+
header: Header,
52+
list: List,
53+
embed: Embed,
54+
image: {
55+
class: Image,
56+
config: {
57+
uploader: {
58+
uploadByFile(file) {
59+
// Implement your file upload logic here
60+
return Promise.resolve({
61+
success: 1,
62+
file: {
63+
url: 'https://example.com/image.png',
64+
}
65+
});
66+
}
67+
}
68+
}
69+
},
70+
code: Code,
71+
},
72+
data: changelogData.content,
73+
onChange: async () => {
74+
const content = await editor.save();
75+
setChangelogData(prev => ({ ...prev, content }));
76+
},
77+
});
78+
79+
editorRef.current = editor;
80+
}
81+
};
82+
2783
const fetchChangelogData = async () => {
2884
try {
2985
const response = await fetch(`/api/auth/changelog/fetch-changelog?changelogId=${params.id}`, {
@@ -35,8 +91,13 @@ const ChangelogEditor = ({ params }) => {
3591
throw new Error('Failed to fetch changelog data');
3692
}
3793
const data = await response.json();
94+
data.tags = data.tags || [];
95+
data.content = JSON.parse(data.content || '{}');
3896
setChangelogData(data);
39-
setIsDraft(data.draft); // Assuming the API returns a 'draft' field
97+
setIsDraft(data.draft);
98+
if (editorRef.current) {
99+
editorRef.current.render(data.content);
100+
}
40101
} catch (error) {
41102
toast({
42103
title: "Error",
@@ -66,6 +127,13 @@ const ChangelogEditor = ({ params }) => {
66127
}
67128
};
68129

130+
const handleKeyPress = (e) => {
131+
if (e.key === 'Enter' && newTag) {
132+
e.preventDefault();
133+
handleAddTag();
134+
}
135+
};
136+
69137
const handleRemoveTag = (tagToRemove) => {
70138
setChangelogData(prev => ({
71139
...prev,
@@ -78,10 +146,11 @@ const ChangelogEditor = ({ params }) => {
78146
};
79147

80148
const handleSave = async (saveAsDraft = true) => {
149+
const content = await editorRef.current.save();
81150
const payload = {
82151
changelogId: params.id,
83152
title: changelogData.title,
84-
content: changelogData.content,
153+
content: JSON.stringify(content),
85154
tags: changelogData.tags || [],
86155
draft: saveAsDraft,
87156
coverImage: changelogData.coverImage
@@ -143,76 +212,95 @@ const ChangelogEditor = ({ params }) => {
143212
};
144213

145214
return (
146-
<Card className="w-full mx-auto">
147-
<CardHeader>
148-
<CardTitle>Changelog Editor</CardTitle>
149-
</CardHeader>
215+
<Card className="w-full mx-auto mt-4 border-0">
216+
{/* <CardHeader> */}
217+
{/* <CardTitle>Changelog Editor</CardTitle> */}
218+
{/* </CardHeader> */}
150219
<CardContent>
151-
<div className="space-y-4">
220+
<div className="flex justify-end space-x-2">
221+
{isDraft ? (
222+
<>
223+
<Button variant="outline" onClick={() => handleSave(true)}>
224+
Save as Draft
225+
</Button>
226+
<Button onClick={() => handleSave(false)}>
227+
Publish
228+
</Button>
229+
</>
230+
) : (
231+
<Button onClick={() => handleSave(false)}>
232+
Publish Changes
233+
</Button>
234+
)}
235+
<Button variant="destructive" onClick={handleDelete}>
236+
Delete
237+
</Button>
238+
</div>
239+
<div className="space-y-6">
152240
<Input
153241
type="text"
154242
name="title"
155243
placeholder="Enter title"
156244
value={changelogData.title}
157245
onChange={handleInputChange}
158-
className="font-bold text-xl"
159-
/>
160-
<div>
161-
<label htmlFor="cover-image" className="block text-sm font-medium text-gray-700">
162-
Cover Image
163-
</label>
164-
<FileUploadButton
165-
uploading={false}
166-
setUploading={() => { }}
167-
uploadedFileUrl={changelogData.coverImage || ''}
168-
setUploadedFileUrl={handleImageChange}
169-
/>
170-
</div>
171-
<Textarea
172-
name="content"
173-
placeholder="Enter changelog content (Markdown supported)"
174-
value={changelogData.content}
175-
onChange={handleInputChange}
176-
rows={10}
177-
className="font-mono"
246+
className="font-bold text-2xl border-0 focus:ring-0 focus-visible:ring-0 px-0 mx-0 my-0 py-0 ring-offset-0 focus-visible:ring-offset-0"
178247
/>
179-
<div>
180-
<Input
181-
type="text"
182-
placeholder="Add a tag"
183-
value={newTag}
184-
onChange={(e) => setNewTag(e.target.value)}
185-
className="mb-2"
186-
/>
187-
<Button onClick={handleAddTag} className="mb-2">Add Tag</Button>
188-
<div className="flex flex-wrap gap-2">
189-
{changelogData.tags && changelogData.tags.map((tag, index) => (
190-
<Badge key={index} variant="secondary" className="flex items-center gap-1">
191-
{tag}
192-
<X size={14} onClick={() => handleRemoveTag(tag)} className="cursor-pointer" />
193-
</Badge>
194-
))}
248+
249+
<div className='flex items-start justify-between space-x-4'>
250+
<div className='flex items-center space-x-4 cursor-pointer'
251+
onClick={() => fileInputRef?.current?.click()}
252+
>
253+
<div className="aspect-video bg-gray-100 rounded-lg overflow-hidden h-64">
254+
{changelogData.coverImage ? (
255+
<img
256+
src={changelogData.coverImage}
257+
alt="Cover"
258+
className="w-full h-full object-cover"
259+
/>
260+
) : (
261+
<div className="w-full h-full flex items-center justify-center text-gray-400">
262+
{uploading ? <Icons.spinner className="h-6 w-6 animate-spin" /> : 'Cover image'}
263+
</div>
264+
)}
265+
</div>
266+
<FileUploadButton
267+
innerRef={fileInputRef}
268+
className="hidden"
269+
uploading={uploading}
270+
setUploading={setUploading}
271+
uploadedFileUrl={changelogData.coverImage || ''}
272+
setUploadedFileUrl={handleImageChange}
273+
/>
274+
</div>
275+
276+
<div>
277+
<label className="block text-sm font-medium text-gray-700 mb-2">Tags</label>
278+
<div className="flex flex-wrap gap-2 mb-2">
279+
{changelogData.tags && changelogData.tags.map((tag, index) => (
280+
<Badge key={index} variant="secondary" className="flex items-center gap-1">
281+
{tag}
282+
<X size={14} onClick={() => handleRemoveTag(tag)} className="cursor-pointer" />
283+
</Badge>
284+
))}
285+
</div>
286+
<div className="flex gap-2">
287+
<Input
288+
type="text"
289+
placeholder="Add a tag"
290+
value={newTag}
291+
onChange={(e) => setNewTag(e.target.value)}
292+
onKeyPress={handleKeyPress}
293+
/>
294+
<Button onClick={handleAddTag}>Add Tag</Button>
295+
</div>
195296
</div>
196297
</div>
197-
<div className="flex justify-end space-x-2">
198-
{isDraft ? (
199-
<>
200-
<Button variant="outline" onClick={() => handleSave(true)}>
201-
Save as Draft
202-
</Button>
203-
<Button onClick={() => handleSave(false)}>
204-
Publish
205-
</Button>
206-
</>
207-
) : (
208-
<Button onClick={() => handleSave(false)}>
209-
Publish Changes
210-
</Button>
211-
)}
212-
<Button variant="destructive" onClick={handleDelete}>
213-
Delete
214-
</Button>
298+
299+
{/* https://github.com/codex-team/editor.js/discussions/1910 */}
300+
<div className="prose">
301+
<div id="editorjs" className="rounded-md w-full whateverNameYouWantHere editor-container" />
215302
</div>
303+
216304
</div>
217305
</CardContent>
218306
</Card>

frontend/src/app/[slug]/changelog/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ const Dashboard: React.FC = ({ params }) => {
4343
'Content-Type': 'application/json'
4444
},
4545
body: JSON.stringify({
46-
title: "New Changelog",
47-
content: "New Changelog Content",
46+
title: "Enter title here",
47+
content: "{}",
4848
draft: true
4949
})
5050
})

frontend/src/app/[slug]/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ function Navigation({ items, isActive, setCurrentSection, params, isCollapsed })
130130
if (item.type === 'divider') {
131131
return <div className="border-t border-dashed border-muted w-full"></div>
132132
} else if (item.type === 'label') {
133-
return <div key={item.label} className="text-muted-foreground font-bold text-xs mt-4 w-full flex items-center px-3 py-2">{isCollapsed ? item.label.substring(0, 3) : item.label}</div>
133+
return <div key={item.label} className={cn("text-muted-foreground font-bold text-xs mt-4 w-full flex items-center px-3 py-2", isCollapsed && 'justify-center px-0')}>{isCollapsed ? item.label.substring(0, 3) : item.label}</div>
134134
} else {
135135
return <Link
136136
key={item.href}

frontend/src/components/FileButton.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input';
44
import { Icons } from './icons';
55
import { cn } from '@/lib/utils';
66

7-
export default function FileUploadButton({ uploading, setUploading, uploadedFileUrl, setUploadedFileUrl }) {
7+
export default function FileUploadButton({ uploading, setUploading, uploadedFileUrl, setUploadedFileUrl, className = '', innerRef = null }) {
88
const [selectedFile, setSelectedFile] = useState(null);
99

1010
const handleFileChange = (event) => {
@@ -39,7 +39,7 @@ export default function FileUploadButton({ uploading, setUploading, uploadedFile
3939
};
4040

4141
return (
42-
<div className="flex items-center justify-center space-y-4">
42+
<div className={cn("flex items-center justify-center space-y-4", className)}>
4343
{uploading &&
4444
<Button
4545
disabled={true}
@@ -50,6 +50,7 @@ export default function FileUploadButton({ uploading, setUploading, uploadedFile
5050
</Button>
5151
}
5252
<Input
53+
ref={innerRef}
5354
className={cn(!uploading ? '' : 'hidden')}
5455
type="file"
5556
onChange={handleFileChange}

frontend/tailwind.config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,12 @@ const config = {
8484
},
8585
},
8686
},
87-
plugins: [require("tailwindcss-animate")],
87+
plugins: [
88+
require("tailwindcss-animate"),
89+
require('@tailwindcss/typography')({
90+
className: 'whateverNameYouWantHere',
91+
}),
92+
],
8893
} satisfies Config
8994

9095
export default config

0 commit comments

Comments
 (0)