Skip to content

Commit 0126871

Browse files
Add support for few more item type in blog
1 parent 06a3bc8 commit 0126871

File tree

17 files changed

+688
-76
lines changed

17 files changed

+688
-76
lines changed

app/components/blog/ImageItem.vue

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
<template>
2-
<div class="mb-4 flex justify-center">
3-
<img
4-
:src="imageUrl"
5-
:alt="imageAlt || ''"
6-
class="w-full rounded-lg"
7-
/>
8-
</div>
2+
<figure class="mb-4 flex flex-col items-center">
3+
<img :src="imageUrl" :alt="imageAlt || ''" class="w-full rounded-lg" />
4+
<figcaption v-if="imageAlt" class="mt-2 text-sm text-accent-secondary text-center italic">
5+
{{ imageAlt }}
6+
</figcaption>
7+
</figure>
98
</template>
109

1110
<script setup lang="ts">
@@ -14,4 +13,3 @@ const props = defineProps<{
1413
imageAlt?: string
1514
}>()
1615
</script>
17-

app/components/blog/LinkItem.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template>
2+
<div class="mb-4">
3+
<a :href="linkUrl" class="inline-flex items-center text-accent-primary hover:underline font-medium"
4+
target="_blank" rel="noopener noreferrer">
5+
{{ linkText }}
6+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24"
7+
stroke="currentColor">
8+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
9+
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
10+
</svg>
11+
</a>
12+
</div>
13+
</template>
14+
15+
<script setup lang="ts">
16+
defineProps<{
17+
linkUrl: string
18+
linkText: string
19+
}>()
20+
</script>

app/components/blog/TagsItem.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<template>
2+
<div class="mb-6 flex flex-wrap gap-2">
3+
<MainBadge v-for="tag in tags" :key="tag" variant="info" :label="tag" />
4+
</div>
5+
</template>
6+
7+
<script setup lang="ts">
8+
import MainBadge from '~~/app/components/MainBadge.vue'
9+
10+
defineProps<{
11+
tags: string[]
12+
}>()
13+
</script>

app/components/blog/TextItem.vue

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
</code>
2020
</template>
2121
<template v-else-if="part.type === 'link'">
22-
<a :href="part.url" class="text-accent-primary underline hover:text-accent-primary/80" target="_blank" rel="noopener noreferrer">
22+
<a :href="part.url" class="text-accent-primary underline hover:text-accent-primary/80" target="_blank"
23+
rel="noopener noreferrer">
2324
{{ part.text }}
2425
</a>
2526
</template>
@@ -29,6 +30,10 @@
2930
<template v-else-if="part.type === 'linebreak'">
3031
<br />
3132
</template>
33+
<template v-else-if="part.type === 'image'">
34+
<img :src="part.url" :alt="part.text || ''"
35+
class="inline-block max-w-full h-8 mx-1 align-middle rounded hover:opacity-90 transition-opacity cursor-zoom-in" />
36+
</template>
3237
</span>
3338
</p>
3439
</template>
@@ -41,7 +46,7 @@ const props = defineProps<{
4146
}>()
4247
4348
interface TextPart {
44-
type: 'text' | 'bold' | 'italic' | 'boldItalic' | 'code' | 'link' | 'strikethrough' | 'linebreak'
49+
type: 'text' | 'bold' | 'italic' | 'boldItalic' | 'code' | 'link' | 'strikethrough' | 'linebreak' | 'image'
4550
content?: string
4651
text?: string
4752
url?: string
@@ -50,7 +55,7 @@ interface TextPart {
5055
const parsedParts = computed((): TextPart[] => {
5156
const parts: TextPart[] = []
5257
const text = props.text
53-
58+
5459
if (!text) {
5560
return parts
5661
}
@@ -66,51 +71,54 @@ const parsedParts = computed((): TextPart[] => {
6671
const findMatches = (regex: RegExp, type: string) => {
6772
const matches: Array<{ type: string; start: number; end: number; groups: RegExpMatchArray }> = []
6873
let match: RegExpExecArray | null
69-
74+
7075
// Reset regex lastIndex
7176
regex.lastIndex = 0
72-
77+
7378
while ((match = regex.exec(text)) !== null) {
7479
matches.push({
7580
type,
7681
start: match.index,
7782
end: match.index + match[0].length,
7883
groups: match
7984
})
80-
85+
8186
// Prevent infinite loop on zero-length matches
8287
if (match[0].length === 0) {
8388
regex.lastIndex++
8489
}
8590
}
86-
91+
8792
return matches
8893
}
8994
9095
// Find all matches for each pattern type
9196
// Order matters: process more specific patterns first (e.g., boldItalic before bold/italic)
9297
const allMatches: Array<{ type: string; start: number; end: number; groups: RegExpMatchArray }> = []
93-
98+
9499
// Bold+Italic (***text*** or ___text___)
95100
allMatches.push(...findMatches(/(\*\*\*|___)(.+?)\1/g, 'boldItalic'))
96-
101+
97102
// Code (inline code - process first to avoid parsing inside code blocks)
98103
allMatches.push(...findMatches(/`([^`\n]+)`/g, 'code'))
99-
104+
100105
// Links ([text](url))
101106
allMatches.push(...findMatches(/\[([^\]]+)\]\(([^)]+)\)/g, 'link'))
102-
107+
108+
// Inline Images (![alt](url))
109+
allMatches.push(...findMatches(/!\[([^\]]*)\]\(([^)]+)\)/g, 'image'))
110+
103111
// Strikethrough (~~text~~)
104112
allMatches.push(...findMatches(/~~(.+?)~~/g, 'strikethrough'))
105-
113+
106114
// Bold (**text** or __text__)
107115
// Note: We check these are not part of boldItalic by processing boldItalic first and filtering overlaps
108116
allMatches.push(...findMatches(/(\*\*|__)([^*_\n]+?)\1/g, 'bold'))
109-
117+
110118
// Italic (*text* or _text_)
111119
// Must have word boundary to avoid matching parts of bold or code
112120
allMatches.push(...findMatches(/(?<![*_])(?<!\w)(\*|_)([^*_\n]+?)\1(?![*_])(?!\w)/g, 'italic'))
113-
121+
114122
// Line breaks (two spaces + newline or double newline)
115123
allMatches.push(...findMatches(/ \n|\n\n/g, 'linebreak'))
116124
@@ -122,17 +130,17 @@ const parsedParts = computed((): TextPart[] => {
122130
for (let i = 0; i < allMatches.length; i++) {
123131
const current = allMatches[i] as { type: string; start: number; end: number; groups: RegExpMatchArray }
124132
let shouldAdd = true
125-
133+
126134
// Check if current overlaps with any already filtered match
127135
for (let j = filteredMatches.length - 1; j >= 0; j--) {
128136
const existing = filteredMatches[j] as { type: string; start: number; end: number; groups: RegExpMatchArray }
129137
const overlaps = !(current.end <= existing.start || current.start >= existing.end)
130-
138+
131139
if (overlaps) {
132140
// If current is longer, replace the existing match
133141
const currentLength = current.end - current.start
134142
const existingLength = existing.end - existing.start
135-
143+
136144
if (currentLength > existingLength) {
137145
filteredMatches.splice(j, 1)
138146
} else {
@@ -142,24 +150,24 @@ const parsedParts = computed((): TextPart[] => {
142150
}
143151
}
144152
}
145-
153+
146154
if (shouldAdd) {
147155
filteredMatches.push(current)
148156
}
149157
}
150-
158+
151159
// Re-sort after filtering (positions may have changed)
152160
filteredMatches.sort((a, b) => a.start - b.start)
153161
154162
// Build parts array
155163
let lastPos = 0
156-
164+
157165
for (const match of filteredMatches) {
158166
// Add text before this match
159167
if (match.start > lastPos) {
160168
addText(text.substring(lastPos, match.start))
161169
}
162-
170+
163171
// Add the matched part
164172
switch (match.type) {
165173
case 'boldItalic':
@@ -177,17 +185,20 @@ const parsedParts = computed((): TextPart[] => {
177185
case 'link':
178186
parts.push({ type: 'link', text: match.groups[1], url: match.groups[2] })
179187
break
188+
case 'image':
189+
parts.push({ type: 'image', text: match.groups[1], url: match.groups[2] })
190+
break
180191
case 'strikethrough':
181192
parts.push({ type: 'strikethrough', content: match.groups[1] })
182193
break
183194
case 'linebreak':
184195
parts.push({ type: 'linebreak' })
185196
break
186197
}
187-
198+
188199
lastPos = match.end
189200
}
190-
201+
191202
// Add remaining text
192203
if (lastPos < text.length) {
193204
addText(text.substring(lastPos))
@@ -201,4 +212,3 @@ const parsedParts = computed((): TextPart[] => {
201212
return parts
202213
})
203214
</script>
204-

app/pages/admin/blog/[slug].vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
<textarea v-model="markdown" rows="24" placeholder="markdown"
2424
class="w-full p-3 rounded bg-transparent border border-separator-primary text-label-primary font-mono"></textarea>
2525
<div class="flex items-center gap-2">
26-
<button class="bg-accent-primary text-white px-4 py-2 rounded hover:opacity-90" @click="save"
27-
:disabled="saving">{{ saving ? 'Saving…' : 'Save' }}</button>
26+
<MainButton @click="save" :disabled="saving">{{ saving ? 'Saving…' : 'Save' }}</MainButton>
2827
<span v-if="saveMsg" class="text-green-600">{{ saveMsg }}</span>
2928
<span v-if="errorMsg" class="text-red-500">{{ errorMsg }}</span>
3029
</div>

app/pages/admin/blog/index.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
</select>
1616
<textarea v-model="createForm.markdown" rows="8" placeholder="markdown"
1717
class="w-full p-2 rounded bg-transparent border border-separator-primary text-label-primary"></textarea>
18-
<button class="bg-accent-primary text-white px-4 py-2 rounded hover:opacity-90"
19-
:disabled="creating">{{ creating ? 'Creating…' : 'Create' }}</button>
18+
<MainButton type="submit" :disabled="creating">
19+
{{ creating ? 'Creating…' : 'Create' }}
20+
</MainButton>
2021
</form>
2122
<p v-if="createError" class="text-red-500 mt-2">{{ createError }}</p>
2223
<p v-if="createOk" class="text-green-600 mt-2">Created</p>

app/pages/blog/[article_slug].vue

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,20 @@
33
<div v-if="pending" class="text-center py-12">
44
<p class="text-accent-primary">Loading article...</p>
55
</div>
6-
6+
77
<div v-else-if="error" class="text-center py-12">
88
<p class="text-red-500">Error loading article: {{ error.message }}</p>
99
</div>
10-
11-
<div v-else-if="content">
10+
11+
<div v-else-if="content" class="space-y-4">
1212
<template v-for="(block, index) in content.blocks" :key="index">
13-
<BlogHeaderItem
14-
v-if="block.type === 'header'"
15-
:title="block.title || ''"
16-
:size="block.size || '1'"
17-
/>
18-
<BlogTextItem
19-
v-else-if="block.type === 'text'"
20-
:text="block.content || ''"
21-
/>
22-
<BlogImageItem
23-
v-else-if="block.type === 'image'"
24-
:image-url="block.imageUrl || ''"
25-
:image-alt="block.imageAlt || ''"
26-
/>
13+
<BlogHeaderItem v-if="block.type === 'header'" :title="block.title || ''" :size="block.size || '1'" />
14+
<BlogTextItem v-else-if="block.type === 'text'" :text="block.content || ''" />
15+
<BlogImageItem v-else-if="block.type === 'image'" :image-url="block.imageUrl || ''"
16+
:image-alt="block.imageAlt || ''" />
17+
<BlogLinkItem v-else-if="block.type === 'link'" :link-url="block.linkUrl || ''"
18+
:link-text="block.linkText || ''" />
19+
<BlogTagsItem v-else-if="block.type === 'tags'" :tags="block.tags || []" />
2720
</template>
2821
</div>
2922
</div>
@@ -51,4 +44,3 @@ const { data: content, pending, error } = await useFetch<ContentResponse>(
5144
`/api/content/${mdFileName}`
5245
)
5346
</script>
54-

app/pages/design/index.vue

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<template>
2+
<div class="min-h-screen bg-background-primary">
3+
<div class="container mx-auto px-6 py-16">
4+
<h1 class="text-4xl font-bold mb-8 text-accent-primary">
5+
Design System
6+
</h1>
7+
<p class="text-xl mb-16 text-accent-primary opacity-80">
8+
Central hub for all design system components and guidelines.
9+
</p>
10+
11+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
12+
<!-- Buttons Card -->
13+
<NuxtLink to="/design/buttons" class="block group h-full">
14+
<div
15+
class="p-6 bg-fill-secondary rounded-xl border border-separator-primary hover:border-accent-primary transition-colors h-full flex flex-col">
16+
<h2
17+
class="text-2xl font-semibold mb-4 text-accent-primary group-hover:opacity-80 transition-opacity">
18+
Buttons
19+
</h2>
20+
<div class="flex flex-wrap gap-2 mb-6 pointer-events-none">
21+
<MainButton buttonStyle="primary" size="S" label="Primary" />
22+
<MainButton buttonStyle="secondary" size="S" label="Secondary" />
23+
</div>
24+
<p class="text-accent-primary opacity-70 mt-auto">
25+
Button styles, sizes, and states.
26+
</p>
27+
</div>
28+
</NuxtLink>
29+
30+
<!-- Badges Card -->
31+
<NuxtLink to="/design/badges" class="block group h-full">
32+
<div
33+
class="p-6 bg-fill-secondary rounded-xl border border-separator-primary hover:border-accent-primary transition-colors h-full flex flex-col">
34+
<h2
35+
class="text-2xl font-semibold mb-4 text-accent-primary group-hover:opacity-80 transition-opacity">
36+
Badges
37+
</h2>
38+
<div class="flex flex-wrap gap-2 mb-6 pointer-events-none">
39+
<MainBadge variant="info" label="Info" />
40+
<MainBadge variant="success" label="Success" />
41+
</div>
42+
<p class="text-accent-primary opacity-70 mt-auto">
43+
Status indicators and labels.
44+
</p>
45+
</div>
46+
</NuxtLink>
47+
</div>
48+
</div>
49+
</div>
50+
</template>
51+
52+
<script setup lang="ts">
53+
import { onMounted } from 'vue'
54+
import { trackEvent } from '~~/app/utils/track'
55+
56+
onMounted(() => {
57+
trackEvent('page_view', { page: 'design_system_index' })
58+
})
59+
</script>

app/pages/home/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const cards = [
4444
description: 'Переглянути рейтинг та карму всіх учасників спільноти',
4545
},
4646
{
47-
route: '/design/buttons',
47+
route: '/design',
4848
icon: 'mdi:palette',
4949
title: 'Подивитися на дизайн систему',
5050
description: 'Вивчити компоненти та стилі дизайн системи',

0 commit comments

Comments
 (0)