Skip to content

Commit 56e42fe

Browse files
committed
✨ Add Content Creation Modal and Markdown Support
- Implement create content modal with options for forum, articles, and discussions - Add markdown editor for article creation using @uiw/react-md-editor - Integrate react-markdown and remark-gfm for markdown rendering - Add @heroicons/react and @tailwindcss/typography for improved UI - Enhance navbar with create content button and slide-over component - Update package dependencies to support new features
1 parent 7f19488 commit 56e42fe

10 files changed

Lines changed: 2898 additions & 210 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ node ace serve --watch
5555

5656
## Features
5757

58-
- [ ] User Authentication
59-
- [ ] GitHub Authentication
58+
- [x] User Authentication
59+
- [x] GitHub Authentication
6060
- [ ] Twitter Integration
6161
- [ ] Article Management
6262
- [ ] Forum

inertia/app/app.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/// <reference path="../../adonisrc.ts" />
22
/// <reference path="../../config/inertia.ts" />
33

4-
import '../css/app.css';
4+
import '../css/app.css'
55
import { hydrateRoot } from 'react-dom/client'
6-
import { createInertiaApp } from '@inertiajs/react';
6+
import { createInertiaApp } from '@inertiajs/react'
77
import { resolvePageComponent } from '@adonisjs/inertia/helpers'
88

99
const appName = import.meta.env.VITE_APP_NAME || 'AdonisJS'
@@ -14,15 +14,10 @@ createInertiaApp({
1414
title: (title) => `${title} - ${appName}`,
1515

1616
resolve: (name) => {
17-
return resolvePageComponent(
18-
`../pages/${name}.tsx`,
19-
import.meta.glob('../pages/**/*.tsx'),
20-
)
17+
return resolvePageComponent(`../pages/${name}.tsx`, import.meta.glob('../pages/**/*.tsx'))
2118
},
2219

2320
setup({ el, App, props }) {
24-
2521
hydrateRoot(el, <App {...props} />)
26-
2722
},
28-
});
23+
})
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { FormEvent, useState, Suspense, lazy } from 'react'
2+
import { useForm } from '@inertiajs/react'
3+
import { Switch } from '@headlessui/react'
4+
import SlideOver from '../slide-over'
5+
6+
const MDEditor = lazy(() => import('@uiw/react-md-editor'))
7+
8+
interface CreateArticleFormProps {
9+
isOpen: boolean
10+
onClose: () => void
11+
}
12+
13+
export default function CreateArticleForm({ isOpen, onClose }: CreateArticleFormProps) {
14+
const [isDraft, setIsDraft] = useState(true)
15+
const { data, setData, post, processing, errors } = useForm({
16+
title: '',
17+
canonicalUrl: '',
18+
content: '',
19+
excerpt: '',
20+
tags: [],
21+
language: 'fr',
22+
coverImage: null as File | null,
23+
})
24+
25+
function handleSubmit(e: FormEvent) {
26+
e.preventDefault()
27+
post('/articles', {
28+
onSuccess: () => onClose(),
29+
})
30+
}
31+
32+
return (
33+
<SlideOver isOpen={isOpen} onClose={onClose} title="Rédiger un article">
34+
<div className="space-y-6">
35+
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4">
36+
<div className="flex">
37+
<div className="flex-shrink-0">
38+
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
39+
<path
40+
fillRule="evenodd"
41+
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
42+
clipRule="evenodd"
43+
/>
44+
</svg>
45+
</div>
46+
<div className="ml-3">
47+
<p className="text-sm text-yellow-700">
48+
Soumettez votre article au site Laravel.cm. Nous recherchons des articles de haute
49+
qualité autour de JavaScript, Node.js, React, Next.js, Nest.js, AdonisJS, Angular,
50+
Vue, CSS et autres sujets connexes. Les articles ne peuvent pas être de nature
51+
promotionnelle et doivent être éducatifs et informatifs.
52+
</p>
53+
</div>
54+
</div>
55+
</div>
56+
57+
<form onSubmit={handleSubmit} className="space-y-6">
58+
<div className="grid grid-cols-3 gap-6">
59+
<div className="col-span-2">
60+
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
61+
Titre<span className="text-red-500">*</span>
62+
</label>
63+
<input
64+
type="text"
65+
name="title"
66+
id="title"
67+
value={data.title}
68+
onChange={(e) => setData('title', e.target.value)}
69+
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
70+
/>
71+
{errors.title && <p className="mt-1 text-sm text-red-600">{errors.title}</p>}
72+
</div>
73+
74+
<div>
75+
<label htmlFor="language" className="block text-sm font-medium text-gray-700">
76+
Langue<span className="text-red-500">*</span>
77+
</label>
78+
<div className="mt-1 flex space-x-4">
79+
<button
80+
type="button"
81+
onClick={() => setData('language', 'en')}
82+
className={`px-3 py-1 rounded-md ${
83+
data.language === 'en'
84+
? 'bg-gray-900 text-white'
85+
: 'bg-gray-100 text-gray-900 hover:bg-gray-200'
86+
}`}
87+
>
88+
En
89+
</button>
90+
<button
91+
type="button"
92+
onClick={() => setData('language', 'fr')}
93+
className={`px-3 py-1 rounded-md ${
94+
data.language === 'fr'
95+
? 'bg-gray-900 text-white'
96+
: 'bg-gray-100 text-gray-900 hover:bg-gray-200'
97+
}`}
98+
>
99+
Fr
100+
</button>
101+
</div>
102+
</div>
103+
</div>
104+
105+
<div>
106+
<label htmlFor="excerpt" className="block text-sm font-medium text-gray-700">
107+
Excerpt<span className="text-red-500">*</span>
108+
<span className="ml-1 text-xs text-gray-500">(minimum 50 caractères)</span>
109+
</label>
110+
<textarea
111+
id="excerpt"
112+
rows={3}
113+
value={data.excerpt}
114+
onChange={(e) => setData('excerpt', e.target.value)}
115+
className={`mt-1 block w-full rounded-md border ${
116+
data.excerpt.length > 0 && data.excerpt.length < 50
117+
? 'border-yellow-300'
118+
: 'border-gray-300'
119+
} px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm`}
120+
/>
121+
<div className="mt-1 flex justify-between">
122+
<span
123+
className={`text-xs ${data.excerpt.length < 50 ? 'text-yellow-600' : 'text-green-600'}`}
124+
>
125+
{data.excerpt.length} / 50 caractères minimum
126+
</span>
127+
{errors.excerpt && <p className="text-sm text-red-600">{errors.excerpt}</p>}
128+
</div>
129+
</div>
130+
131+
<div>
132+
<label htmlFor="content" className="block text-sm font-medium text-gray-700">
133+
Contenu<span className="text-red-500">*</span>
134+
<span className="ml-1 text-xs text-gray-500">(minimum 100 caractères)</span>
135+
</label>
136+
<div className="mt-1" data-color-mode="light">
137+
<Suspense>
138+
<MDEditor
139+
value={data.content}
140+
onChange={(value) => setData('content', value || '')}
141+
preview="edit"
142+
height={400}
143+
/>
144+
</Suspense>
145+
<div className="mt-1 flex justify-between">
146+
<span
147+
className={`text-xs ${data.content.length < 100 ? 'text-yellow-600' : 'text-green-600'}`}
148+
>
149+
{data.content.length} / 100 caractères minimum
150+
</span>
151+
{errors.content && <p className="text-sm text-red-600">{errors.content}</p>}
152+
</div>
153+
</div>
154+
</div>
155+
156+
<div className="flex items-center justify-between">
157+
<Switch.Group>
158+
<div className="flex items-center">
159+
<Switch
160+
checked={!isDraft}
161+
onChange={() => setIsDraft(!isDraft)}
162+
className={`${
163+
!isDraft ? 'bg-indigo-600' : 'bg-gray-200'
164+
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2`}
165+
>
166+
<span
167+
className={`${
168+
!isDraft ? 'translate-x-6' : 'translate-x-1'
169+
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
170+
/>
171+
</Switch>
172+
<Switch.Label className="ml-3 text-sm text-gray-600">
173+
{isDraft ? 'Brouillon' : 'Prêt à publier'}
174+
</Switch.Label>
175+
</div>
176+
</Switch.Group>
177+
178+
<div className="flex gap-x-3">
179+
<button
180+
type="button"
181+
onClick={onClose}
182+
className="rounded-md bg-white py-2 px-3 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
183+
>
184+
Annuler
185+
</button>
186+
<button
187+
type="submit"
188+
disabled={processing}
189+
className="inline-flex justify-center rounded-md bg-indigo-600 py-2 px-3 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
190+
>
191+
{isDraft ? 'Sauvegarder comme brouillon' : 'Publier'}
192+
</button>
193+
</div>
194+
</div>
195+
</form>
196+
</div>
197+
</SlideOver>
198+
)
199+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { Dialog, Transition } from '@headlessui/react'
2+
import { Fragment, useState } from 'react'
3+
import { Link } from '@inertiajs/react'
4+
import CreateArticleForm from './articles/create-form'
5+
6+
interface CreateContentModalProps {
7+
isOpen: boolean
8+
onClose: () => void
9+
}
10+
11+
export default function CreateContentModal({ isOpen, onClose }: CreateContentModalProps) {
12+
const [isArticleFormOpen, setIsArticleFormOpen] = useState(false)
13+
14+
return (
15+
<>
16+
<Transition appear show={isOpen} as={Fragment}>
17+
<Dialog as="div" className="relative z-50" onClose={onClose}>
18+
<Transition.Child
19+
as={Fragment}
20+
enter="ease-out duration-300"
21+
enterFrom="opacity-0"
22+
enterTo="opacity-100"
23+
leave="ease-in duration-200"
24+
leaveFrom="opacity-100"
25+
leaveTo="opacity-0"
26+
>
27+
<div className="fixed inset-0 bg-black/25" />
28+
</Transition.Child>
29+
30+
<div className="fixed inset-0 overflow-y-auto">
31+
<div className="flex min-h-full items-center justify-center p-4 text-center">
32+
<Transition.Child
33+
as={Fragment}
34+
enter="ease-out duration-300"
35+
enterFrom="opacity-0 scale-95"
36+
enterTo="opacity-100 scale-100"
37+
leave="ease-in duration-200"
38+
leaveFrom="opacity-100 scale-100"
39+
leaveTo="opacity-0 scale-95"
40+
>
41+
<Dialog.Panel className="w-full max-w-xl transform overflow-hidden rounded-lg bg-white p-6 text-left align-middle shadow-xl transition-all">
42+
<div className="space-y-6">
43+
<Link
44+
href="/forum/create"
45+
className="flex items-center space-x-3 p-4 hover:bg-gray-50 rounded-lg"
46+
>
47+
<div className="flex-shrink-0">
48+
<svg
49+
className="h-6 w-6 text-emerald-500"
50+
viewBox="0 0 24 24"
51+
fill="currentColor"
52+
>
53+
<path
54+
fillRule="evenodd"
55+
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm-1 13v-2H9a1 1 0 110-2h2V9a1 1 0 112 0v2h2a1 1 0 110 2h-2v2a1 1 0 11-2 0z"
56+
clipRule="evenodd"
57+
/>
58+
</svg>
59+
</div>
60+
<div>
61+
<h3 className="text-base font-medium text-gray-900">Créer un sujet</h3>
62+
<p className="mt-1 text-sm text-gray-500">
63+
Vous avez une question? Posez là dans le forum
64+
</p>
65+
</div>
66+
</Link>
67+
68+
<Link
69+
onClick={(e) => {
70+
e.preventDefault()
71+
onClose()
72+
setIsArticleFormOpen(true)
73+
}}
74+
href="/articles/create"
75+
className="flex items-center space-x-3 p-4 hover:bg-gray-50 rounded-lg"
76+
>
77+
<div className="flex-shrink-0">
78+
<svg
79+
className="h-6 w-6 text-emerald-500"
80+
viewBox="0 0 24 24"
81+
fill="currentColor"
82+
>
83+
<path
84+
fillRule="evenodd"
85+
d="M19.5 21a3 3 0 003-3v-4.5a3 3 0 00-3-3h-15a3 3 0 00-3 3V18a3 3 0 003 3h15zM1.5 10.146V6a3 3 0 013-3h15a3 3 0 013 3v4.146A4.483 4.483 0 0019.5 9h-15a4.483 4.483 0 00-3 1.146z"
86+
clipRule="evenodd"
87+
/>
88+
</svg>
89+
</div>
90+
<div>
91+
<h3 className="text-base font-medium text-gray-900">Rédiger un article</h3>
92+
<p className="mt-1 text-sm text-gray-500">
93+
Partagez vos découvertes à des milliers de développeurs
94+
</p>
95+
</div>
96+
</Link>
97+
98+
<Link
99+
href="/discussions/create"
100+
className="flex items-center space-x-3 p-4 hover:bg-gray-50 rounded-lg"
101+
>
102+
<div className="flex-shrink-0">
103+
<svg
104+
className="h-6 w-6 text-emerald-500"
105+
viewBox="0 0 24 24"
106+
fill="currentColor"
107+
>
108+
<path
109+
fillRule="evenodd"
110+
d="M4.848 2.771A49.144 49.144 0 0112 2.25c2.43 0 4.817.178 7.152.52 1.978.292 3.348 2.024 3.348 3.97v6.02c0 1.946-1.37 3.678-3.348 3.97a48.901 48.901 0 01-3.476.383.39.39 0 00-.297.17l-2.755 4.133a.75.75 0 01-1.248 0l-2.755-4.133a.39.39 0 00-.297-.17 48.9 48.9 0 01-3.476-.384c-1.978-.29-3.348-2.024-3.348-3.97V6.741c0-1.946 1.37-3.68 3.348-3.97zM6.75 8.25a.75.75 0 01.75-.75h9a.75.75 0 010 1.5h-9a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5H12a.75.75 0 000-1.5H7.5z"
111+
clipRule="evenodd"
112+
/>
113+
</svg>
114+
</div>
115+
<div>
116+
<h3 className="text-base font-medium text-gray-900">
117+
Démarrer une discussion
118+
</h3>
119+
<p className="mt-1 text-sm text-gray-500">
120+
Échangez, débattez sur différentes thématiques et idées.
121+
</p>
122+
</div>
123+
</Link>
124+
</div>
125+
126+
<div className="mt-6">
127+
<button
128+
type="button"
129+
className="inline-flex justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium text-gray-900 hover:bg-gray-50 focus:outline-none"
130+
onClick={onClose}
131+
>
132+
Annuler
133+
</button>
134+
</div>
135+
</Dialog.Panel>
136+
</Transition.Child>
137+
</div>
138+
</div>
139+
</Dialog>
140+
</Transition>
141+
142+
<CreateArticleForm isOpen={isArticleFormOpen} onClose={() => setIsArticleFormOpen(false)} />
143+
</>
144+
)
145+
}

0 commit comments

Comments
 (0)