Электронные визитки: таблица + API + публичная страница /card/:slug + раздел в админке с загрузкой фото
This commit is contained in:
@@ -12,7 +12,7 @@ dotenv.config();
|
||||
const app = express();
|
||||
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
app.use(express.json({ limit: '6mb' }));
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use(
|
||||
|
||||
@@ -231,3 +231,106 @@ adminRouter.put('/settings/:key', requireAdmin, async (req, res) => {
|
||||
);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// ============ Электронные визитки ============
|
||||
const cardSchema = z.object({
|
||||
slug: z.string().min(1).max(64).regex(/^[a-z0-9-]+$/, 'slug_format'),
|
||||
name: z.string().min(1).max(128),
|
||||
role: z.string().max(128).nullable().optional(),
|
||||
company: z.string().max(128).nullable().optional(),
|
||||
phone: z.string().max(64).nullable().optional(),
|
||||
email: z.string().max(128).nullable().optional(),
|
||||
telegram: z.string().max(128).nullable().optional(),
|
||||
photo: z.string().max(5_000_000).nullable().optional(),
|
||||
product_title: z.string().max(128).nullable().optional(),
|
||||
product_subtitle: z.string().max(256).nullable().optional(),
|
||||
product_url: z.string().max(256).nullable().optional(),
|
||||
site_url: z.string().max(256).nullable().optional(),
|
||||
is_published: z.boolean().optional(),
|
||||
});
|
||||
|
||||
adminRouter.get('/cards', requireAdmin, async (_req, res) => {
|
||||
const cards = await query(`SELECT * FROM business_cards ORDER BY created_at ASC`);
|
||||
res.json({ cards });
|
||||
});
|
||||
|
||||
adminRouter.post('/cards', requireAdmin, async (req, res) => {
|
||||
const parsed = cardSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: 'invalid_payload' });
|
||||
return;
|
||||
}
|
||||
const c = parsed.data;
|
||||
try {
|
||||
const created = await queryOne(
|
||||
`INSERT INTO business_cards
|
||||
(slug, name, role, company, phone, email, telegram, photo,
|
||||
product_title, product_subtitle, product_url, site_url, is_published)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
||||
RETURNING *`,
|
||||
[
|
||||
c.slug, c.name, c.role ?? null, c.company ?? null, c.phone ?? null,
|
||||
c.email ?? null, c.telegram ?? null, c.photo ?? null,
|
||||
c.product_title ?? null, c.product_subtitle ?? null,
|
||||
c.product_url ?? null, c.site_url ?? null, c.is_published ?? true,
|
||||
]
|
||||
);
|
||||
res.status(201).json({ card: created });
|
||||
} catch (err: any) {
|
||||
if (err?.code === '23505') {
|
||||
res.status(409).json({ error: 'slug_exists' });
|
||||
return;
|
||||
}
|
||||
console.error('[POST /cards]', err);
|
||||
res.status(500).json({ error: 'internal_error' });
|
||||
}
|
||||
});
|
||||
|
||||
adminRouter.put('/cards/:id', requireAdmin, async (req, res) => {
|
||||
const id = parseInt(String(req.params.id), 10);
|
||||
if (Number.isNaN(id)) {
|
||||
res.status(400).json({ error: 'invalid_id' });
|
||||
return;
|
||||
}
|
||||
const parsed = cardSchema.partial().safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: 'invalid_payload' });
|
||||
return;
|
||||
}
|
||||
const c = parsed.data;
|
||||
const updated = await queryOne(
|
||||
`UPDATE business_cards SET
|
||||
slug = COALESCE($2, slug),
|
||||
name = COALESCE($3, name),
|
||||
role = COALESCE($4, role),
|
||||
company = COALESCE($5, company),
|
||||
phone = COALESCE($6, phone),
|
||||
email = COALESCE($7, email),
|
||||
telegram = COALESCE($8, telegram),
|
||||
photo = COALESCE($9, photo),
|
||||
product_title = COALESCE($10, product_title),
|
||||
product_subtitle = COALESCE($11, product_subtitle),
|
||||
product_url = COALESCE($12, product_url),
|
||||
site_url = COALESCE($13, site_url),
|
||||
is_published = COALESCE($14, is_published),
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING *`,
|
||||
[
|
||||
id, c.slug, c.name, c.role, c.company, c.phone, c.email, c.telegram,
|
||||
c.photo, c.product_title, c.product_subtitle, c.product_url,
|
||||
c.site_url, c.is_published,
|
||||
]
|
||||
);
|
||||
if (!updated) {
|
||||
res.status(404).json({ error: 'not_found' });
|
||||
return;
|
||||
}
|
||||
res.json({ card: updated });
|
||||
});
|
||||
|
||||
adminRouter.delete('/cards/:id', requireAdmin, async (req, res) => {
|
||||
const id = parseInt(String(req.params.id), 10);
|
||||
await query(`DELETE FROM business_cards WHERE id = $1`, [id]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
@@ -51,3 +51,23 @@ publicRouter.get('/content', async (_req, res) => {
|
||||
res.status(500).json({ error: 'internal_error' });
|
||||
}
|
||||
});
|
||||
|
||||
publicRouter.get('/cards/:slug', async (req, res) => {
|
||||
try {
|
||||
const card = await query(
|
||||
`SELECT slug, name, role, company, phone, email, telegram, photo,
|
||||
product_title, product_subtitle, product_url, site_url
|
||||
FROM business_cards
|
||||
WHERE slug = $1 AND is_published = true`,
|
||||
[req.params.slug]
|
||||
);
|
||||
if (!card[0]) {
|
||||
res.status(404).json({ error: 'not_found' });
|
||||
return;
|
||||
}
|
||||
res.json({ card: card[0] });
|
||||
} catch (err) {
|
||||
console.error('[GET /cards/:slug]', err);
|
||||
res.status(500).json({ error: 'internal_error' });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user