Skip to content

Commit 233b7c3

Browse files
committed
implemented About Page Content
1 parent 3e9c986 commit 233b7c3

File tree

17 files changed

+1019
-39
lines changed

17 files changed

+1019
-39
lines changed

TASKS.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# TASKS: Dynamic About Details Feature
2+
3+
## Overview
4+
Create a new "About Details" admin tile that allows updating the About page sections through a form. This involves adding a database table, API routes, admin form component with localization, and updating the About pages to fetch dynamic content.
5+
6+
---
7+
8+
## Tasks
9+
10+
### 1. Database Table Creation
11+
**File:** `backend/src/database/init.ts`
12+
13+
- [ ] Create `about_sections` table with fields:
14+
- `id` INTEGER PRIMARY KEY AUTOINCREMENT
15+
- `sectionKey` TEXT NOT NULL (e.g., 'header', 'intro', 'approach', 'expertise', 'personal')
16+
- `language` TEXT NOT NULL ('en' or 'pl')
17+
- `title` TEXT
18+
- `body` TEXT (HTML string)
19+
- `createdAt` DATETIME DEFAULT CURRENT_TIMESTAMP
20+
- `updatedAt` DATETIME DEFAULT CURRENT_TIMESTAMP
21+
- [ ] Add UNIQUE constraint on (sectionKey, language) combination
22+
23+
---
24+
25+
### 2. Database Queries
26+
**File:** `backend/src/database/queries.ts`
27+
28+
- [ ] Add `getAboutSections()` - fetch all sections
29+
- [ ] Add `getAboutSectionsByLanguage(language: string)` - fetch sections for specific language
30+
- [ ] Add `getAboutSection(sectionKey: string, language: string)` - fetch single section
31+
- [ ] Add `upsertAboutSection(section)` - create or update a section
32+
- [ ] Add `deleteAboutSection(id: number)` - delete a section
33+
34+
---
35+
36+
### 3. API Routes
37+
**File:** `backend/src/routes/about.ts` (new file)
38+
39+
- [ ] Create Express router
40+
- [ ] GET `/api/about` - public, fetch all sections
41+
- [ ] GET `/api/about/:language` - public, fetch sections by language
42+
- [ ] PUT `/api/about` - protected, upsert section (requires admin auth)
43+
- [ ] DELETE `/api/about/:id` - protected, delete section
44+
45+
**File:** `backend/src/server.ts`
46+
47+
- [ ] Import and register about routes
48+
49+
---
50+
51+
### 4. Translation Files
52+
**File:** `frontend/admin/public/locales/en/translation.json`
53+
54+
- [ ] Add `aboutDetails` tile title and description under `settings.tiles`
55+
- [ ] Add form labels: section names, title field, body field, save/cancel buttons
56+
- [ ] Add success/error messages
57+
58+
**File:** `frontend/admin/public/locales/pl/translation.json`
59+
60+
- [ ] Add Polish translations for all above keys
61+
62+
---
63+
64+
### 5. Editor Component
65+
**File:** `frontend/src/admin/components/AboutDetailsEditor.jsx` (new file)
66+
67+
- [ ] Create form with language tabs (EN / PL)
68+
- [ ] For each section (header, intro, approach, expertise, personal):
69+
- [ ] Title input field
70+
- [ ] Body textarea (accepts HTML)
71+
- [ ] Fetch existing content on mount
72+
- [ ] Save button calls API to upsert
73+
- [ ] Show success/error toast notifications
74+
75+
---
76+
77+
### 6. Modal Components
78+
**File:** `frontend/src/admin/components/modals/AboutDetailsModalDesktop.jsx` (new file)
79+
80+
- [ ] Create modal wrapper using base Modal component
81+
- [ ] Embed AboutDetailsEditor component
82+
83+
**File:** `frontend/src/admin/components/modals/AboutDetailsModalMobile.jsx` (new file)
84+
85+
- [ ] Create mobile-friendly modal version
86+
87+
**File:** `frontend/src/admin/components/modals/index.js`
88+
89+
- [ ] Export new modal components
90+
91+
---
92+
93+
### 7. Settings Page Update
94+
**File:** `frontend/src/admin/pages/Settings.jsx`
95+
96+
- [ ] Add new tile config to `settingsTiles` array:
97+
```javascript
98+
{
99+
id: 'aboutDetails',
100+
title: t('settings.tiles.aboutDetails.title'),
101+
description: t('settings.tiles.aboutDetails.description'),
102+
icon: 'ℹ️',
103+
color: '#9b59b6'
104+
}

backend/src/database/init.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,20 @@ export const initializeDatabase = (): Promise<void> => {
346346
)
347347
`);
348348

349+
// Create ABOUT_SECTIONS table for dynamic About page content
350+
db.run(`
351+
CREATE TABLE IF NOT EXISTS about_sections (
352+
id INTEGER PRIMARY KEY AUTOINCREMENT,
353+
sectionKey TEXT NOT NULL,
354+
language TEXT NOT NULL DEFAULT 'en',
355+
title TEXT,
356+
body TEXT,
357+
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
358+
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
359+
UNIQUE(sectionKey, language)
360+
)
361+
`);
362+
349363
// Ensure startHour/endHour columns exist (safe migration for existing DBs)
350364

351365
db.all(`PRAGMA table_info('settings')`, (err, rows: any[]) => {

backend/src/database/queries.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,4 +616,57 @@ export class DatabaseQueries {
616616
});
617617
}
618618

619+
// ABOUT SECTIONS
620+
static getAboutSections(): Promise<any[]> {
621+
return new Promise((resolve, reject) => {
622+
db.all(`SELECT * FROM about_sections ORDER BY sectionKey, language`, (err, rows) => {
623+
if (err) reject(err);
624+
else resolve(rows as any[]);
625+
});
626+
});
627+
}
628+
629+
static getAboutSectionsByLanguage(language: string): Promise<any[]> {
630+
return new Promise((resolve, reject) => {
631+
db.all(`SELECT * FROM about_sections WHERE language = ? ORDER BY sectionKey`, [language], (err, rows) => {
632+
if (err) reject(err);
633+
else resolve(rows as any[]);
634+
});
635+
});
636+
}
637+
638+
static getAboutSection(sectionKey: string, language: string): Promise<any | null> {
639+
return new Promise((resolve, reject) => {
640+
db.get(`SELECT * FROM about_sections WHERE sectionKey = ? AND language = ?`, [sectionKey, language], (err, row) => {
641+
if (err) reject(err);
642+
else resolve(row || null);
643+
});
644+
});
645+
}
646+
647+
static upsertAboutSection(section: { sectionKey: string; language: string; title: string; body: string }): Promise<number> {
648+
return new Promise((resolve, reject) => {
649+
db.run(`
650+
INSERT INTO about_sections (sectionKey, language, title, body)
651+
VALUES (?, ?, ?, ?)
652+
ON CONFLICT(sectionKey, language) DO UPDATE SET
653+
title = excluded.title,
654+
body = excluded.body,
655+
updatedAt = CURRENT_TIMESTAMP
656+
`, [section.sectionKey, section.language, section.title, section.body], function(err) {
657+
if (err) reject(err);
658+
else resolve(this.lastID || this.changes);
659+
});
660+
});
661+
}
662+
663+
static deleteAboutSection(id: number): Promise<void> {
664+
return new Promise((resolve, reject) => {
665+
db.run(`DELETE FROM about_sections WHERE id = ?`, [id], (err) => {
666+
if (err) reject(err);
667+
else resolve();
668+
});
669+
});
670+
}
671+
619672
}

backend/src/routes/about.ts

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import express from 'express';
2+
import { DatabaseQueries } from '../database';
3+
import { ApiResponse } from '../types';
4+
import { authenticateAdmin } from '../middleware/auth';
5+
6+
const router = express.Router();
7+
8+
// GET /api/about - Get all about sections - PUBLIC
9+
router.get('/', async (req, res) => {
10+
try {
11+
const sections = await DatabaseQueries.getAboutSections();
12+
13+
const response: ApiResponse = {
14+
success: true,
15+
data: sections,
16+
message: `Found ${sections.length} about sections`
17+
};
18+
19+
res.json(response);
20+
} catch (error) {
21+
console.error('Error fetching about sections:', error);
22+
res.status(500).json({
23+
success: false,
24+
error: 'Failed to fetch about sections',
25+
message: error instanceof Error ? error.message : 'Unknown error'
26+
});
27+
}
28+
});
29+
30+
// GET /api/about/:language - Get about sections by language - PUBLIC
31+
router.get('/:language', async (req, res) => {
32+
try {
33+
const { language } = req.params;
34+
35+
if (!['en', 'pl', 'default'].includes(language)) {
36+
return res.status(400).json({
37+
success: false,
38+
error: 'Invalid language. Supported: en, pl, default'
39+
});
40+
}
41+
42+
const sections = await DatabaseQueries.getAboutSectionsByLanguage(language);
43+
44+
// Transform array into object keyed by sectionKey for easier frontend consumption
45+
const sectionsMap: Record<string, { title: string; body: string }> = {};
46+
sections.forEach((section: any) => {
47+
sectionsMap[section.sectionKey] = {
48+
title: section.title,
49+
body: section.body
50+
};
51+
});
52+
53+
const response: ApiResponse = {
54+
success: true,
55+
data: sectionsMap,
56+
message: `Found ${sections.length} about sections for ${language}`
57+
};
58+
59+
res.json(response);
60+
} catch (error) {
61+
console.error('Error fetching about sections by language:', error);
62+
res.status(500).json({
63+
success: false,
64+
error: 'Failed to fetch about sections',
65+
message: error instanceof Error ? error.message : 'Unknown error'
66+
});
67+
}
68+
});
69+
70+
// PUT /api/about - Create or update about section - PROTECTED
71+
router.put('/', authenticateAdmin, async (req, res) => {
72+
try {
73+
const { sectionKey, language, title, body } = req.body;
74+
75+
if (!sectionKey || !language) {
76+
return res.status(400).json({
77+
success: false,
78+
error: 'sectionKey and language are required'
79+
});
80+
}
81+
82+
if (!['en', 'pl', 'default'].includes(language)) {
83+
return res.status(400).json({
84+
success: false,
85+
error: 'Invalid language. Supported: en, pl, default'
86+
});
87+
}
88+
89+
const validSections = ['header', 'intro', 'approach', 'expertise', 'personal'];
90+
if (!validSections.includes(sectionKey)) {
91+
return res.status(400).json({
92+
success: false,
93+
error: `Invalid sectionKey. Supported: ${validSections.join(', ')}`
94+
});
95+
}
96+
97+
await DatabaseQueries.upsertAboutSection({
98+
sectionKey,
99+
language,
100+
title: title || '',
101+
body: body || ''
102+
});
103+
104+
res.json({
105+
success: true,
106+
message: `About section '${sectionKey}' for '${language}' saved successfully`
107+
});
108+
} catch (error) {
109+
console.error('Error saving about section:', error);
110+
res.status(500).json({
111+
success: false,
112+
error: 'Failed to save about section',
113+
message: error instanceof Error ? error.message : 'Unknown error'
114+
});
115+
}
116+
});
117+
118+
// PUT /api/about/bulk - Bulk update multiple sections - PROTECTED
119+
router.put('/bulk', authenticateAdmin, async (req, res) => {
120+
try {
121+
const { sections } = req.body;
122+
123+
if (!Array.isArray(sections) || sections.length === 0) {
124+
return res.status(400).json({
125+
success: false,
126+
error: 'sections array is required'
127+
});
128+
}
129+
130+
const validSections = ['header', 'intro', 'approach', 'expertise', 'personal'];
131+
const validLanguages = ['en', 'pl', 'default'];
132+
133+
for (const section of sections) {
134+
if (!section.sectionKey || !section.language) {
135+
return res.status(400).json({
136+
success: false,
137+
error: 'Each section must have sectionKey and language'
138+
});
139+
}
140+
141+
if (!validLanguages.includes(section.language)) {
142+
return res.status(400).json({
143+
success: false,
144+
error: `Invalid language '${section.language}'. Supported: ${validLanguages.join(', ')}`
145+
});
146+
}
147+
148+
if (!validSections.includes(section.sectionKey)) {
149+
return res.status(400).json({
150+
success: false,
151+
error: `Invalid sectionKey '${section.sectionKey}'. Supported: ${validSections.join(', ')}`
152+
});
153+
}
154+
}
155+
156+
// Save all sections
157+
for (const section of sections) {
158+
await DatabaseQueries.upsertAboutSection({
159+
sectionKey: section.sectionKey,
160+
language: section.language,
161+
title: section.title || '',
162+
body: section.body || ''
163+
});
164+
}
165+
166+
res.json({
167+
success: true,
168+
message: `Saved ${sections.length} about sections successfully`
169+
});
170+
} catch (error) {
171+
console.error('Error bulk saving about sections:', error);
172+
res.status(500).json({
173+
success: false,
174+
error: 'Failed to save about sections',
175+
message: error instanceof Error ? error.message : 'Unknown error'
176+
});
177+
}
178+
});
179+
180+
// DELETE /api/about/:id - Delete about section - PROTECTED
181+
router.delete('/:id', authenticateAdmin, async (req, res) => {
182+
try {
183+
const id = parseInt(req.params.id);
184+
185+
if (isNaN(id)) {
186+
return res.status(400).json({
187+
success: false,
188+
error: 'Invalid section ID'
189+
});
190+
}
191+
192+
await DatabaseQueries.deleteAboutSection(id);
193+
194+
res.json({
195+
success: true,
196+
message: 'About section deleted successfully'
197+
});
198+
} catch (error) {
199+
console.error('Error deleting about section:', error);
200+
res.status(500).json({
201+
success: false,
202+
error: 'Failed to delete about section',
203+
message: error instanceof Error ? error.message : 'Unknown error'
204+
});
205+
}
206+
});
207+
208+
export default router;

0 commit comments

Comments
 (0)