feat: publishing blog, ndx in router, introduce actions
This commit is contained in:
parent
7f0431f8f8
commit
d4148ed01d
28
package-lock.json
generated
28
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "degmods.com",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.0-alpha-1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "degmods.com",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.0-alpha-1",
|
||||
"dependencies": {
|
||||
"@getalby/lightning-tools": "5.0.3",
|
||||
"@nostr-dev-kit/ndk": "2.10.0",
|
||||
@ -39,6 +39,7 @@
|
||||
"react-toastify": "10.0.5",
|
||||
"react-window": "1.8.10",
|
||||
"swiper": "11.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"uuid": "10.0.0",
|
||||
"webln": "0.3.2"
|
||||
},
|
||||
@ -51,6 +52,7 @@
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
@ -1038,6 +1040,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mixmark-io/domino": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
||||
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
|
||||
@ -2032,6 +2040,13 @@
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/turndown": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz",
|
||||
"integrity": "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
|
||||
@ -5149,6 +5164,15 @@
|
||||
"resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.16.tgz",
|
||||
"integrity": "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw=="
|
||||
},
|
||||
"node_modules/turndown": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz",
|
||||
"integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mixmark-io/domino": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||
|
@ -41,6 +41,7 @@
|
||||
"react-toastify": "10.0.5",
|
||||
"react-window": "1.8.10",
|
||||
"swiper": "11.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"uuid": "10.0.0",
|
||||
"webln": "0.3.2"
|
||||
},
|
||||
@ -53,6 +54,7 @@
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import { useEffect } from 'react'
|
||||
import { router } from 'routes'
|
||||
import { routerWithNdkContext } from 'routes'
|
||||
import { useNDKContext } from 'hooks'
|
||||
import './styles/styles.css'
|
||||
|
||||
function App() {
|
||||
const ndkContext = useNDKContext()
|
||||
|
||||
useEffect(() => {
|
||||
// Find the element with id 'root'
|
||||
const rootElement = document.getElementById('root')
|
||||
@ -21,7 +24,7 @@ function App() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <RouterProvider router={router} />
|
||||
return <RouterProvider router={routerWithNdkContext(ndkContext)} />
|
||||
}
|
||||
|
||||
export default App
|
||||
|
@ -298,7 +298,55 @@ const MenuBarButton = ({
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`btn btnMain btnMainTipTap ${isActive ? 'is-active' : ''}`}
|
||||
type='button'
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
|
||||
interface InputFieldUncontrolledProps extends React.ComponentProps<'input'> {
|
||||
label: string
|
||||
description?: string
|
||||
error?: string
|
||||
}
|
||||
/**
|
||||
* Uncontrolled input component with design classes, label, description and error support
|
||||
*
|
||||
* Extends {@link React.ComponentProps<'input'> React.ComponentProps<'input'>}
|
||||
* @param label
|
||||
* @param description
|
||||
* @param error
|
||||
*
|
||||
* @see {@link React.ComponentProps<'input'>}
|
||||
*/
|
||||
export const InputFieldUncontrolled = ({
|
||||
label,
|
||||
description,
|
||||
error,
|
||||
...rest
|
||||
}: InputFieldUncontrolledProps) => (
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label htmlFor={rest.id} className='form-label labelMain'>
|
||||
{label}
|
||||
</label>
|
||||
{description && <p className='labelDescriptionMain'>{description}</p>}
|
||||
<input className='inputMain' {...rest} />
|
||||
{error && <InputError message={error} />}
|
||||
</div>
|
||||
)
|
||||
|
||||
interface CheckboxFieldUncontrolledProps extends React.ComponentProps<'input'> {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const CheckboxFieldUncontrolled = ({
|
||||
label,
|
||||
...rest
|
||||
}: CheckboxFieldUncontrolledProps) => (
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
|
||||
<label htmlFor={rest.id} className='form-label labelMain'>
|
||||
{label}
|
||||
</label>
|
||||
<input type='checkbox' className='CheckboxMain' {...rest} />
|
||||
</div>
|
||||
)
|
||||
|
@ -32,7 +32,7 @@ type FetchModsOptions = {
|
||||
author?: string
|
||||
}
|
||||
|
||||
interface NDKContextType {
|
||||
export interface NDKContextType {
|
||||
ndk: NDK
|
||||
fetchMods: (opts: FetchModsOptions) => Promise<ModDetails[]>
|
||||
fetchEvents: (filter: NDKFilter) => Promise<NDKEvent[]>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NDKContext } from 'contexts/NDKContext'
|
||||
import { NDKContext, NDKContextType } from 'contexts/NDKContext'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export const useNDKContext = () => {
|
||||
@ -9,5 +9,5 @@ export const useNDKContext = () => {
|
||||
'NDKContext should not be used in out component tree hierarchy'
|
||||
)
|
||||
|
||||
return { ...ndkContext }
|
||||
return { ...ndkContext } as NDKContextType
|
||||
}
|
||||
|
1
src/pages/blog.tsx
Normal file
1
src/pages/blog.tsx
Normal file
@ -0,0 +1 @@
|
||||
export const BlogPage = () => <>WIP</>
|
@ -1,75 +0,0 @@
|
||||
import { CheckboxField, InputField } from '../components/Inputs'
|
||||
import { ProfileSection } from '../components/ProfileSection'
|
||||
import { useAppSelector } from '../hooks'
|
||||
import '../styles/innerPage.css'
|
||||
import '../styles/styles.css'
|
||||
import '../styles/write.css'
|
||||
|
||||
export const WritePage = () => {
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||
<div className='IBMSMSplitMain'>
|
||||
<div className='IBMSMSplitMainBigSide'>
|
||||
<div className='IBMSMTitleMain'>
|
||||
<h2 className='IBMSMTitleMainHeading'>
|
||||
Write a blog post (WIP)
|
||||
</h2>
|
||||
</div>
|
||||
<div className='IBMSMSMBS_Write'>
|
||||
<InputField
|
||||
label='Title'
|
||||
placeholder=''
|
||||
name='title'
|
||||
value=''
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<InputField
|
||||
label='Body'
|
||||
placeholder=''
|
||||
name='body'
|
||||
value=''
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<InputField
|
||||
label='Featured Image URL'
|
||||
placeholder=''
|
||||
name='imageUrl'
|
||||
inputMode='url'
|
||||
value=''
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<InputField
|
||||
label='Summary'
|
||||
placeholder=''
|
||||
name='summary'
|
||||
type='textarea'
|
||||
value=''
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<CheckboxField
|
||||
label='This mod not safe for work (NSFW)'
|
||||
name='nsfw'
|
||||
isChecked={false}
|
||||
handleChange={() => {}}
|
||||
type='stylized'
|
||||
/>
|
||||
<div className='IBMSMSMBS_WriteAction'>
|
||||
<button className='btn btnMain' type='button'>
|
||||
Publish
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{userState.auth && userState.user?.pubkey && (
|
||||
<ProfileSection pubkey={userState.user.pubkey as string} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
162
src/pages/write/action.tsx
Normal file
162
src/pages/write/action.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import { NDKContextType } from 'contexts/NDKContext'
|
||||
import { ActionFunctionArgs, redirect } from 'react-router-dom'
|
||||
import { getBlogPageRoute } from 'routes'
|
||||
import { BlogFormErrors, BlogFormSubmit } from 'types'
|
||||
import {
|
||||
isReachable,
|
||||
isValidImageUrl,
|
||||
log,
|
||||
LogType,
|
||||
now,
|
||||
parseFormData
|
||||
} from 'utils'
|
||||
import TurndownService from 'turndown'
|
||||
import { kinds, UnsignedEvent, Event, nip19 } from 'nostr-tools'
|
||||
import { toast } from 'react-toastify'
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { store } from 'store'
|
||||
|
||||
export const writeRouteAction =
|
||||
(ndkContext: NDKContextType) =>
|
||||
async ({ params, request }: ActionFunctionArgs) => {
|
||||
// Get the current state
|
||||
const userState = store.getState().user
|
||||
let hexPubkey: string
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
hexPubkey = userState.user.pubkey as string
|
||||
} else {
|
||||
try {
|
||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
log(true, LogType.Error, 'Failed to get public key.', error)
|
||||
}
|
||||
|
||||
toast.error('Failed to get public key.')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (!hexPubkey) {
|
||||
toast.error('Could not get pubkey')
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the form data from submit request
|
||||
const formData = await request.formData()
|
||||
|
||||
// Parse the the data
|
||||
const formSubmit = parseFormData<BlogFormSubmit>(formData)
|
||||
|
||||
// Check for errors
|
||||
const formErrors = await validateFormData(formSubmit)
|
||||
|
||||
// Return earily if there are any errors
|
||||
if (Object.keys(formErrors).length) return formErrors
|
||||
|
||||
// Get the markdown from the html
|
||||
const turndownService = new TurndownService()
|
||||
const content = turndownService.turndown(formSubmit.content!)
|
||||
|
||||
const uuid = uuidv4()
|
||||
const currentTimeStamp = now()
|
||||
|
||||
const aTag = `${kinds.ClassifiedListing}:${hexPubkey}:${uuid}`
|
||||
const tTags = formSubmit
|
||||
.tags!.toLowerCase()
|
||||
.split(',')
|
||||
.map((t) => ['t', t])
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
kind: kinds.LongFormArticle,
|
||||
created_at: currentTimeStamp,
|
||||
pubkey: hexPubkey,
|
||||
content: content,
|
||||
tags: [
|
||||
['d', uuid],
|
||||
['a', aTag],
|
||||
['r', window.location.host],
|
||||
['published_at', currentTimeStamp.toString()],
|
||||
['title', formSubmit.title!],
|
||||
['image', formSubmit.image!],
|
||||
['summary', formSubmit.summary!],
|
||||
['nsfw', (formSubmit.nsfw === 'on').toString()],
|
||||
...tTags
|
||||
]
|
||||
}
|
||||
|
||||
try {
|
||||
const signedEvent = await window.nostr
|
||||
?.signEvent(unsignedEvent)
|
||||
.then((event) => event as Event)
|
||||
|
||||
if (!signedEvent) {
|
||||
toast.error('Failed to sign the event!')
|
||||
return null
|
||||
}
|
||||
|
||||
const ndkEvent = new NDKEvent(ndkContext.ndk, signedEvent)
|
||||
const publishedOnRelays = await ndkContext.publish(ndkEvent)
|
||||
|
||||
// Handle cases where publishing failed or succeeded
|
||||
if (publishedOnRelays.length === 0) {
|
||||
toast.error('Failed to publish event on any relay.')
|
||||
return null
|
||||
} else {
|
||||
toast.success(
|
||||
`Event published successfully to the following relays\n\n${publishedOnRelays.join(
|
||||
'\n'
|
||||
)}`
|
||||
)
|
||||
const naddr = nip19.naddrEncode({
|
||||
identifier: aTag,
|
||||
pubkey: signedEvent.pubkey,
|
||||
kind: signedEvent.kind,
|
||||
relays: publishedOnRelays
|
||||
})
|
||||
return redirect(getBlogPageRoute(naddr))
|
||||
}
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, 'Failed to sign the event!', error)
|
||||
toast.error('Failed to sign the event!')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const validateFormData = async (
|
||||
formData: Partial<BlogFormSubmit>
|
||||
): Promise<BlogFormErrors> => {
|
||||
const errors: BlogFormErrors = {}
|
||||
|
||||
if (!formData.title || formData.title.trim() === '') {
|
||||
errors.title = 'Title field can not be empty'
|
||||
}
|
||||
|
||||
if (
|
||||
!formData.content ||
|
||||
formData.content.trim() === '' ||
|
||||
formData.content.trim() === '<p></p>'
|
||||
) {
|
||||
errors.content = 'Content field can not be empty'
|
||||
}
|
||||
|
||||
if (!formData.summary || formData.summary.trim() === '') {
|
||||
errors.summary = 'Summary field can not be empty'
|
||||
}
|
||||
|
||||
if (!formData.tags || formData.tags.trim() === '') {
|
||||
errors.tags = 'Tags field can not be empty'
|
||||
}
|
||||
|
||||
if (!formData.image || formData.image.trim() === '') {
|
||||
errors.image = 'Image url field can not be empty'
|
||||
} else if (
|
||||
!isValidImageUrl(formData.image) ||
|
||||
!(await isReachable(formData.image))
|
||||
) {
|
||||
errors.image = 'Image must be a valid and reachable'
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
105
src/pages/write/index.tsx
Normal file
105
src/pages/write/index.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { useState } from 'react'
|
||||
import { Form, useActionData, useNavigation } from 'react-router-dom'
|
||||
import {
|
||||
CheckboxFieldUncontrolled,
|
||||
InputField,
|
||||
InputFieldUncontrolled
|
||||
} from '../../components/Inputs'
|
||||
import { ProfileSection } from '../../components/ProfileSection'
|
||||
import { useAppSelector } from '../../hooks'
|
||||
import { BlogFormErrors } from 'types'
|
||||
import '../../styles/innerPage.css'
|
||||
import '../../styles/styles.css'
|
||||
import '../../styles/write.css'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
|
||||
export const WritePage = () => {
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const formErrors = useActionData() as BlogFormErrors
|
||||
const navigation = useNavigation()
|
||||
const title = 'Submit a blog post'
|
||||
|
||||
const [content, setContent] = useState<string>('')
|
||||
const handleContentChange = (_: string, value: string) => {
|
||||
setContent(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||
<div className='IBMSMSplitMain'>
|
||||
<div className='IBMSMSplitMainBigSide'>
|
||||
<div className='IBMSMTitleMain'>
|
||||
<h2 className='IBMSMTitleMainHeading'>{title}</h2>
|
||||
</div>
|
||||
{navigation.state === 'loading' && (
|
||||
<LoadingSpinner desc='Loading..' />
|
||||
)}
|
||||
{navigation.state === 'submitting' && (
|
||||
<LoadingSpinner desc='Publishing blog to relays' />
|
||||
)}
|
||||
<Form className='IBMSMSMBS_Write' method='post'>
|
||||
<InputFieldUncontrolled
|
||||
label='Title'
|
||||
name='title'
|
||||
error={formErrors?.title}
|
||||
/>
|
||||
<InputField
|
||||
label='Content'
|
||||
name={'content'}
|
||||
type='richtext'
|
||||
placeholder='Blog content'
|
||||
value={content}
|
||||
error={formErrors?.content}
|
||||
onChange={handleContentChange}
|
||||
/>
|
||||
<input name='content' hidden value={content} readOnly />
|
||||
<InputFieldUncontrolled
|
||||
label='Featured Image URL'
|
||||
name='image'
|
||||
inputMode='url'
|
||||
error={formErrors?.image}
|
||||
/>
|
||||
<InputFieldUncontrolled
|
||||
label='Summary'
|
||||
name='summary'
|
||||
type='textarea'
|
||||
error={formErrors?.summary}
|
||||
/>
|
||||
<InputFieldUncontrolled
|
||||
label='Tags'
|
||||
description='Separate each tag with a comma. (Example: tag1, tag2, tag3)'
|
||||
placeholder='Tags'
|
||||
name='tags'
|
||||
error={formErrors?.tags}
|
||||
/>
|
||||
<CheckboxFieldUncontrolled
|
||||
label='This post is not safe for work (NSFW)'
|
||||
name='nsfw'
|
||||
/>
|
||||
<div className='IBMSMSMBS_WriteAction'>
|
||||
<button
|
||||
className='btn btnMain'
|
||||
type='submit'
|
||||
disabled={
|
||||
navigation.state === 'loading' ||
|
||||
navigation.state === 'submitting'
|
||||
}
|
||||
>
|
||||
{navigation.state === 'submitting'
|
||||
? 'Publishing...'
|
||||
: 'Publish'}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
{userState.auth && userState.user?.pubkey && (
|
||||
<ProfileSection pubkey={userState.user.pubkey as string} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { createBrowserRouter } from 'react-router-dom'
|
||||
import { NDKContextType } from 'contexts/NDKContext'
|
||||
import { Layout } from 'layout'
|
||||
import { SearchPage } from 'pages/search'
|
||||
import { SearchPage } from '../pages/search'
|
||||
import { AboutPage } from '../pages/about'
|
||||
import { BlogsPage } from '../pages/blogs'
|
||||
import { GamesPage } from '../pages/games'
|
||||
@ -10,12 +11,14 @@ import { ModsPage } from '../pages/mods'
|
||||
import { ProfilePage } from '../pages/profile'
|
||||
import { SettingsPage } from '../pages/settings'
|
||||
import { SubmitModPage } from '../pages/submitMod'
|
||||
import { GamePage } from '../pages/game'
|
||||
import { NotFoundPage } from '../pages/404'
|
||||
import { FeedLayout } from '../layout/feed'
|
||||
import { FeedPage } from '../pages/feed'
|
||||
import { NotificationsPage } from '../pages/notifications'
|
||||
import { WritePage } from '../pages/write'
|
||||
import { GamePage } from 'pages/game'
|
||||
import { NotFoundPage } from 'pages/404'
|
||||
import { FeedLayout } from 'layout/feed'
|
||||
import { FeedPage } from 'pages/feed'
|
||||
import { NotificationsPage } from 'pages/notifications'
|
||||
import { writeRouteAction } from '../pages/write/action'
|
||||
import { BlogPage } from 'pages/blog'
|
||||
|
||||
export const appRoutes = {
|
||||
index: '/',
|
||||
@ -25,7 +28,8 @@ export const appRoutes = {
|
||||
mods: '/mods',
|
||||
mod: '/mod/:naddr',
|
||||
about: '/about',
|
||||
blog: '/blog',
|
||||
blogs: '/blogs',
|
||||
blog: '/blog/:naddr',
|
||||
submitMod: '/submit-mod',
|
||||
editMod: '/edit-mod/:naddr',
|
||||
write: '/write',
|
||||
@ -48,10 +52,14 @@ export const getModPageRoute = (eventId: string) =>
|
||||
export const getModsEditPageRoute = (eventId: string) =>
|
||||
appRoutes.editMod.replace(':naddr', eventId)
|
||||
|
||||
export const getBlogPageRoute = (eventId: string) =>
|
||||
appRoutes.blog.replace(':naddr', eventId)
|
||||
|
||||
export const getProfilePageRoute = (nprofile: string) =>
|
||||
appRoutes.profile.replace(':nprofile', nprofile)
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
export const routerWithNdkContext = (context: NDKContextType) =>
|
||||
createBrowserRouter([
|
||||
{
|
||||
element: <Layout />,
|
||||
children: [
|
||||
@ -80,9 +88,13 @@ export const router = createBrowserRouter([
|
||||
element: <AboutPage />
|
||||
},
|
||||
{
|
||||
path: appRoutes.blog,
|
||||
path: appRoutes.blogs,
|
||||
element: <BlogsPage />
|
||||
},
|
||||
{
|
||||
path: appRoutes.blog,
|
||||
element: <BlogPage />
|
||||
},
|
||||
{
|
||||
path: appRoutes.submitMod,
|
||||
element: <SubmitModPage />
|
||||
@ -93,7 +105,8 @@ export const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: appRoutes.write,
|
||||
element: <WritePage />
|
||||
element: <WritePage />,
|
||||
action: writeRouteAction(context)
|
||||
},
|
||||
{
|
||||
path: appRoutes.search,
|
||||
|
23
src/types/blog.ts
Normal file
23
src/types/blog.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export interface BlogDetails {
|
||||
title: string
|
||||
content: string
|
||||
summary: string
|
||||
image: string
|
||||
nsfw: boolean
|
||||
tags: string
|
||||
|
||||
id: string
|
||||
author: string
|
||||
published_at: number
|
||||
edited_at: number
|
||||
}
|
||||
|
||||
export interface BlogFormSubmit
|
||||
extends Omit<
|
||||
BlogDetails,
|
||||
'nsfw' | 'id' | 'author' | 'published_at' | 'edited_at'
|
||||
> {
|
||||
nsfw: string
|
||||
}
|
||||
|
||||
export interface BlogFormErrors extends Partial<BlogFormSubmit> {}
|
@ -3,3 +3,4 @@ export * from './modsFilter'
|
||||
export * from './nostr'
|
||||
export * from './user'
|
||||
export * from './zap'
|
||||
export * from './blog'
|
||||
|
@ -146,3 +146,13 @@ export const scrollIntoView = (el: HTMLElement | null) => {
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
export const parseFormData = <T>(formData: FormData) => {
|
||||
const result: Partial<T> = {}
|
||||
|
||||
formData.forEach(
|
||||
(value, key) => ((result as Record<string, unknown>)[key] = value as string)
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user