Skip to content

Commit 0d4a739

Browse files
Show documents
1 parent f45a1ee commit 0d4a739

8 files changed

Lines changed: 309 additions & 16 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { readDocumentWithBlocks } from '@/db/api/documents'
2+
import { Document } from '@/components'
3+
4+
const NewsPage = async ({ params }: { params: { slug: string } }) => {
5+
const { slug } = await params
6+
const document = await readDocumentWithBlocks(slug)
7+
8+
return (
9+
<article style={{ flexGrow: 1 }}>
10+
<Document document={document} />
11+
</article>
12+
)
13+
}
14+
15+
export default NewsPage

src/app/(sections)/news/page.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { readDocuments } from '@/db/api/documents'
2+
import Link from 'next/link'
3+
4+
const NewsPage = async () => {
5+
const documents = await readDocuments()
6+
7+
return (
8+
<div>
9+
<h1>Новости</h1>
10+
<ul>
11+
{documents.map((document) => (
12+
<li key={document.id}>
13+
<Link href={`/news/${document.id}`}>{document.title}</Link>
14+
</li>
15+
))}
16+
</ul>
17+
</div>
18+
)
19+
}
20+
21+
export default NewsPage
Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import classes from './document.module.css'
2+
13
type DocumentWithBlocks = {
24
title: string
35
description: string
@@ -11,23 +13,152 @@ type BlockData = {
1113
title?: string
1214
content: string
1315
url?: string
16+
id?: string
17+
created_at?: Date
18+
updated_at?: Date
1419
}
1520

1621
const Document = ({ document }: { document: DocumentWithBlocks }) => {
1722
return (
18-
<div>
23+
<div className={classes.document}>
1924
<h1>{document.title}</h1>
2025
<p>{document.description}</p>
2126
<ul>
22-
{document.blocks.map((block) => (
23-
<li key={block.position}>
24-
<h2>{block.title}</h2>
25-
<p>{block.content}</p>
26-
</li>
27-
))}
27+
{document.blocks.map((block) => {
28+
switch (block.type) {
29+
case 'paragraph':
30+
return (
31+
<DocumentParagraph key={block.id} content={block.content} />
32+
)
33+
case 'heading2':
34+
return <DocumentHeading2 key={block.id} content={block.content} />
35+
case 'heading3':
36+
return <DocumentHeading3 key={block.id} content={block.content} />
37+
case 'list':
38+
return <DocumentList key={block.id} content={block.content} />
39+
case 'youtube':
40+
return <DocumentYoutube key={block.id} url={block.url} />
41+
case 'image':
42+
return <DocumentImage key={block.id} url={block.url} />
43+
case 'quote':
44+
return (
45+
<DocumentQuote
46+
key={block.id}
47+
content={block.content}
48+
url={block.url}
49+
/>
50+
)
51+
default:
52+
return <p>Unknown block type: {block.type}</p>
53+
}
54+
})}
2855
</ul>
2956
</div>
3057
)
3158
}
3259

3360
export default Document
61+
62+
function DocumentParagraph({ content }: { content: string }) {
63+
return <p>{content}</p>
64+
}
65+
66+
function DocumentHeading2({ content }: { content: string }) {
67+
return <h2>{content}</h2>
68+
}
69+
70+
function DocumentHeading3({ content }: { content: string }) {
71+
return <h3>{content}</h3>
72+
}
73+
74+
function DocumentList({ content }: { content: string }) {
75+
// Split content by new lines and render each as a list item
76+
return (
77+
<ul>
78+
{content
79+
.split('\n')
80+
.filter((item) => item.trim().length > 0)
81+
.map((item, idx) => (
82+
<li key={idx}>{item}</li>
83+
))}
84+
</ul>
85+
)
86+
}
87+
88+
function DocumentYoutube({ url }: { url: string }) {
89+
// Convert YouTube URL to embed format
90+
const convertToEmbedUrl = (youtubeUrl: string): string => {
91+
// Handle different YouTube URL formats
92+
const patterns = [
93+
// Patterns to extract the YouTube video ID from various common URL formats:
94+
// 1. Matches standard watch URLs, short youtu.be URLs, and embed URLs.
95+
// 2. Handles watch URLs with additional query parameters.
96+
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]+)/,
97+
/youtube\.com\/watch\?.*v=([a-zA-Z0-9_-]+)/,
98+
]
99+
100+
for (const pattern of patterns) {
101+
const match = youtubeUrl.match(pattern)
102+
if (match && match[1]) {
103+
return `https://www.youtube.com/embed/${match[1]}`
104+
}
105+
}
106+
107+
// If no pattern matches, return the original URL
108+
return youtubeUrl
109+
}
110+
111+
const embedUrl = convertToEmbedUrl(url)
112+
113+
return (
114+
<div>
115+
<iframe
116+
width="560"
117+
height="315"
118+
src={embedUrl}
119+
title="YouTube video player"
120+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
121+
referrerPolicy="strict-origin-when-cross-origin"
122+
allowFullScreen
123+
/>
124+
</div>
125+
)
126+
}
127+
128+
function DocumentImage({ url }: { url: string }) {
129+
return (
130+
<div>
131+
<img src={url} width={300} />
132+
</div>
133+
)
134+
}
135+
136+
function DocumentQuote({ content, url }: { content: string; url: string }) {
137+
return (
138+
<section>
139+
<blockquote>
140+
{content}
141+
{url && (
142+
<footer>
143+
<cite>
144+
<a
145+
href={url}
146+
target="_blank"
147+
rel="noopener noreferrer"
148+
style={{
149+
color: '#555',
150+
textDecoration: 'underline',
151+
fontSize: '0.95em',
152+
marginLeft: '0.5em',
153+
wordBreak: 'break-all',
154+
}}
155+
>
156+
{url}
157+
</a>
158+
</cite>
159+
</footer>
160+
)}
161+
</blockquote>
162+
</section>
163+
)
164+
}

src/components/Document/DocumentBlockForm.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export const DocumentBlockForm = ({
122122
<option value="paragraph">Paragraph</option>
123123
<option value="heading2">Heading 2</option>
124124
<option value="heading3">Heading 3</option>
125-
<option value="list-item">List item</option>
125+
<option value="list">List</option>
126126
<option value="youtube">Youtube</option>
127127
<option value="image">Image</option>
128128
<option value="quote">Quote</option>
@@ -139,7 +139,7 @@ export const DocumentBlockForm = ({
139139
/>
140140
</fieldset>
141141
)}
142-
{['paragraph', 'list-item', 'quote'].includes(currentBlockData.type) && (
142+
{['paragraph', 'list', 'quote'].includes(currentBlockData.type) && (
143143
<fieldset>
144144
<label htmlFor="content">Content</label>
145145
<textarea
@@ -155,9 +155,7 @@ export const DocumentBlockForm = ({
155155
/>
156156
</fieldset>
157157
)}
158-
{['youtube', 'vimeo', 'dailymotion', 'image', 'audio', 'video'].includes(
159-
currentBlockData.type
160-
) && (
158+
{['youtube', 'image', 'quote'].includes(currentBlockData.type) && (
161159
<fieldset>
162160
<label htmlFor="url">URL</label>
163161
<input

src/components/Document/document.module.css

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,116 @@
5959
background: #d9363e;
6060
border-color: #d9363e;
6161
}
62+
63+
.document {
64+
max-width: 800px;
65+
margin: 0 auto;
66+
67+
/** Paragraphs */
68+
p {
69+
font-size: 1.08rem;
70+
line-height: 1.7;
71+
color: #23272f;
72+
margin: 1.1em 0;
73+
word-break: break-word;
74+
}
75+
76+
/** Headings */
77+
h2 {
78+
font-size: 1.6rem;
79+
font-weight: 700;
80+
margin: 2.2em 0 1em 0;
81+
color: #1a1a1a;
82+
letter-spacing: -0.01em;
83+
border-bottom: 2px solid #e5e7eb;
84+
padding-bottom: 0.25em;
85+
}
86+
87+
h3 {
88+
font-size: 1.25rem;
89+
font-weight: 600;
90+
margin: 1.7em 0 0.8em 0;
91+
color: #22223b;
92+
letter-spacing: -0.01em;
93+
}
94+
95+
/** Lists */
96+
ul {
97+
margin: 1.1em 0 1.1em 1.5em;
98+
padding: 0;
99+
list-style-type: disc;
100+
}
101+
102+
ul li {
103+
font-size: 1.05rem;
104+
line-height: 1.6;
105+
margin-bottom: 0.5em;
106+
color: #2d3142;
107+
padding-left: 0.2em;
108+
}
109+
110+
/** Blockquotes */
111+
blockquote {
112+
border-left: 4px solid #a3a3a3;
113+
background: #f7f7fa;
114+
color: #444;
115+
font-style: italic;
116+
margin: 1.5em 0;
117+
padding: 1em 1.5em;
118+
border-radius: 0 8px 8px 0;
119+
position: relative;
120+
}
121+
122+
blockquote footer {
123+
margin-top: 0.7em;
124+
font-size: 0.98em;
125+
color: #6b7280;
126+
}
127+
128+
blockquote cite {
129+
font-style: normal;
130+
color: #4b5563;
131+
}
132+
133+
/** Images */
134+
img {
135+
display: block;
136+
max-width: 100%;
137+
height: auto;
138+
margin: 1.5em auto;
139+
border-radius: 8px;
140+
box-shadow: 0 2px 12px 0 rgba(60, 60, 60, 0.08);
141+
background: #f3f4f6;
142+
}
143+
144+
/** YouTube Video Embeds */
145+
iframe {
146+
display: block;
147+
width: 100%;
148+
max-width: 640px;
149+
aspect-ratio: 16 / 9;
150+
margin: 2em auto;
151+
border: none;
152+
border-radius: 8px;
153+
background: #000;
154+
box-shadow: 0 2px 12px 0 rgba(60, 60, 60, 0.1);
155+
}
156+
157+
/* Responsive adjustments */
158+
@media (max-width: 600px) {
159+
padding: 0 0.5em;
160+
h2 {
161+
font-size: 1.25rem;
162+
}
163+
h3 {
164+
font-size: 1.08rem;
165+
}
166+
iframe {
167+
max-width: 100%;
168+
aspect-ratio: 16 / 9;
169+
}
170+
img {
171+
max-width: 100%;
172+
}
173+
}
174+
}

src/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { RoleForm } from './RoleForm'
66
import { UsersList } from './UsersList/UsersList'
77
import RolesList from './RolesList/RolesList'
88
import MainNavigation from './MainNavigation/MainNavigation'
9+
import Document from './Document/Document'
910
import DocumentForm from './Document/DocumentForm'
1011
import DocumentDelete from './Document/DocumentDelete'
1112

@@ -18,6 +19,7 @@ export {
1819
UsersList,
1920
RolesList,
2021
MainNavigation,
22+
Document,
2123
DocumentForm,
2224
DocumentDelete,
2325
}

src/db/schemas/documents.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ export const documentBlocks = pgTable('document_blocks', {
2020
.notNull()
2121
.references(() => documents.id, { onDelete: 'cascade' }),
2222
position: integer('position').notNull(),
23-
// 'paragraph', 'heading1', 'heading2', 'heading3', 'quote', 'list-item'
24-
// 'youtube', 'vimeo', 'dailymotion', 'image', 'audio', 'video'
23+
// 'paragraph', 'heading2', 'heading3', 'list', 'youtube', 'image', 'quote'
2524
type: text('type').notNull(),
2625
content: text('content'),
2726
url: text('url'),

0 commit comments

Comments
 (0)