feat(qr): complete QR studio, dynamic google fonts, mouse canvas drag, and template sync
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
[
|
||||
{
|
||||
"key": "qr-1",
|
||||
"name": "Lumière Restaurant (QR-1)",
|
||||
"categoryName": "Özel Masa Kartı",
|
||||
"svgTemplateUrl": "/qr-1.svg",
|
||||
"aspectRatio": "1/1",
|
||||
"bgType": "gradient",
|
||||
"bgGradient": "radial-gradient(circle at 50% 50%, #132034 0%, #0b0d13 100%)",
|
||||
"plaqueColor": "#132034",
|
||||
"borderAccent": "#d4af37",
|
||||
"accentText": "#d4af37",
|
||||
"headerText": "#FFFFFF",
|
||||
"subText": "#e5c378",
|
||||
"dividerColor": "rgba(212, 175, 55, 0.3)",
|
||||
"isActive": true,
|
||||
"qrConfig": {
|
||||
"xPercent": 50,
|
||||
"yPercent": 38,
|
||||
"size": 115,
|
||||
"borderRadius": 14,
|
||||
"borderColor": "#D4AF37",
|
||||
"bgBoxColor": "#FFFFFF"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "rName",
|
||||
"text": "{{RESTAURANT_NAME}}",
|
||||
"yPercent": 16,
|
||||
"fontSize": 18,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#D4AF37",
|
||||
"fontWeight": "800",
|
||||
"letterSpacing": 2
|
||||
},
|
||||
{
|
||||
"id": "sub",
|
||||
"text": "RESTAURANT & BAR",
|
||||
"yPercent": 26,
|
||||
"fontSize": 10,
|
||||
"fontFamily": "Plus Jakarta Sans, sans-serif",
|
||||
"color": "#E5C378",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 1.5
|
||||
},
|
||||
{
|
||||
"id": "cta",
|
||||
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
"yPercent": 80,
|
||||
"fontSize": 11,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#FFFFFF",
|
||||
"fontWeight": "700",
|
||||
"letterSpacing": 1
|
||||
},
|
||||
{
|
||||
"id": "table",
|
||||
"text": "MASA NO: 01 • WIFI: Bistro",
|
||||
"yPercent": 90,
|
||||
"fontSize": 9,
|
||||
"fontFamily": "Plus Jakarta Sans, sans-serif",
|
||||
"color": "#A1A1AA",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "qr-2",
|
||||
"name": "The Daily Grind Cafe (QR-2)",
|
||||
"categoryName": "Özel Masa Kartı",
|
||||
"svgTemplateUrl": "/qr-2.svg",
|
||||
"aspectRatio": "400/650",
|
||||
"bgType": "gradient",
|
||||
"bgGradient": "radial-gradient(circle at 50% 50%, #252c36 0%, #171c22 100%)",
|
||||
"plaqueColor": "#f6efe2",
|
||||
"borderAccent": "#3a2312",
|
||||
"accentText": "#8c6239",
|
||||
"headerText": "#2c1a0e",
|
||||
"subText": "#3a2312",
|
||||
"dividerColor": "rgba(140, 98, 57, 0.3)",
|
||||
"isActive": true,
|
||||
"qrConfig": {
|
||||
"xPercent": 50,
|
||||
"yPercent": 36,
|
||||
"size": 125,
|
||||
"borderRadius": 14,
|
||||
"borderColor": "#3a2312",
|
||||
"bgBoxColor": "#FFFFFF"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "rName",
|
||||
"text": "{{RESTAURANT_NAME}}",
|
||||
"yPercent": 14,
|
||||
"fontSize": 18,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#8c6239",
|
||||
"fontWeight": "800",
|
||||
"letterSpacing": 2
|
||||
},
|
||||
{
|
||||
"id": "cta",
|
||||
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
"yPercent": 82,
|
||||
"fontSize": 11,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#2c1a0e",
|
||||
"fontWeight": "700",
|
||||
"letterSpacing": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "qr-3",
|
||||
"name": "Pizzeria Bella Napoli (QR-3)",
|
||||
"categoryName": "Özel Masa Kartı",
|
||||
"svgTemplateUrl": "/qr-3.svg",
|
||||
"aspectRatio": "400/650",
|
||||
"bgType": "gradient",
|
||||
"bgGradient": "radial-gradient(circle at 50% 50%, #faf4e6 0%, #f7eed8 100%)",
|
||||
"plaqueColor": "#fdfbf5",
|
||||
"borderAccent": "#b92b27",
|
||||
"accentText": "#b92b27",
|
||||
"headerText": "#18181b",
|
||||
"subText": "#5d4037",
|
||||
"dividerColor": "rgba(185, 43, 39, 0.3)",
|
||||
"isActive": true,
|
||||
"qrConfig": {
|
||||
"xPercent": 50,
|
||||
"yPercent": 36,
|
||||
"size": 125,
|
||||
"borderRadius": 14,
|
||||
"borderColor": "#b92b27",
|
||||
"bgBoxColor": "#FFFFFF"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "rName",
|
||||
"text": "{{RESTAURANT_NAME}}",
|
||||
"yPercent": 14,
|
||||
"fontSize": 18,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#b92b27",
|
||||
"fontWeight": "800",
|
||||
"letterSpacing": 2
|
||||
},
|
||||
{
|
||||
"id": "cta",
|
||||
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
"yPercent": 82,
|
||||
"fontSize": 11,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#18181b",
|
||||
"fontWeight": "700",
|
||||
"letterSpacing": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "qr-8",
|
||||
"name": "Emerald & Gold Special (QR-8)",
|
||||
"categoryName": "Özel Kare Masa Kartı",
|
||||
"svgTemplateUrl": "/qr-8.svg",
|
||||
"aspectRatio": "1/1",
|
||||
"bgType": "gradient",
|
||||
"bgGradient": "radial-gradient(circle at 50% 50%, #0c4443 0%, #062423 100%)",
|
||||
"plaqueColor": "#f9f5eb",
|
||||
"borderAccent": "#d1a752",
|
||||
"accentText": "#d1a752",
|
||||
"headerText": "#ffffff",
|
||||
"subText": "#d1a752",
|
||||
"dividerColor": "rgba(209, 167, 82, 0.4)",
|
||||
"isActive": true,
|
||||
"qrConfig": {
|
||||
"xPercent": 50,
|
||||
"yPercent": 38,
|
||||
"size": 115,
|
||||
"borderRadius": 14,
|
||||
"borderColor": "#d1a752",
|
||||
"bgBoxColor": "#FFFFFF"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "rName",
|
||||
"text": "{{RESTAURANT_NAME}}",
|
||||
"yPercent": 16,
|
||||
"fontSize": 18,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#d1a752",
|
||||
"fontWeight": "800"
|
||||
},
|
||||
{
|
||||
"id": "cta",
|
||||
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
"yPercent": 80,
|
||||
"fontSize": 11,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#ffffff",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -9,6 +9,7 @@ import { menuItemsRoutes } from "./routes/menu-items.js";
|
||||
import { menusRoutes } from "./routes/menus.js";
|
||||
import { meRoutes } from "./routes/me.js";
|
||||
import { qrRoutes } from "./routes/qr.js";
|
||||
import { qrTemplatesRoutes } from "./routes/qr-templates.js";
|
||||
import { restaurantsRoutes } from "./routes/restaurants.js";
|
||||
import { subscriptionRoutes } from "./routes/subscription.js";
|
||||
|
||||
@@ -42,6 +43,7 @@ await app.register(menusRoutes);
|
||||
await app.register(menuCategoriesRoutes);
|
||||
await app.register(menuItemsRoutes);
|
||||
await app.register(qrRoutes);
|
||||
await app.register(qrTemplatesRoutes);
|
||||
await app.register(aiImportsRoutes);
|
||||
await app.register(domainsRoutes);
|
||||
await app.register(analyticsRoutes);
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { type QrStandPreset } from "@menulio/shared";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DATA_FILE = path.resolve(__dirname, "../data/qr-templates.json");
|
||||
|
||||
export interface DynamicQrTemplate extends QrStandPreset {
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
bgType?: "gradient" | "image" | "svg";
|
||||
bgImageUrl?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
defaultQrConfig?: {
|
||||
xPercent: number;
|
||||
yPercent: number;
|
||||
size: number;
|
||||
borderRadius: number;
|
||||
borderColor: string;
|
||||
bgBoxColor: string;
|
||||
};
|
||||
defaultLayers?: Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
xPercent?: number;
|
||||
yPercent: number;
|
||||
fontSize: number;
|
||||
fontFamily?: string;
|
||||
color: string;
|
||||
fontWeight?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
async function loadTemplatesFromDisk(): Promise<DynamicQrTemplate[]> {
|
||||
try {
|
||||
const content = await fs.readFile(DATA_FILE, "utf-8");
|
||||
const parsed = JSON.parse(content);
|
||||
return parsed.map((p: any) => ({
|
||||
...p,
|
||||
id: p.key,
|
||||
isActive: p.isActive ?? true,
|
||||
createdAt: p.createdAt || new Date().toISOString(),
|
||||
updatedAt: p.updatedAt || new Date().toISOString(),
|
||||
defaultQrConfig: p.defaultQrConfig || p.qrConfig,
|
||||
defaultLayers: p.defaultLayers || p.layers,
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplatesToDisk(templates: DynamicQrTemplate[]): Promise<void> {
|
||||
try {
|
||||
await fs.writeFile(DATA_FILE, JSON.stringify(templates, null, 2), "utf-8");
|
||||
} catch (err) {
|
||||
console.error("Failed to write qr-templates to disk:", err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function qrTemplatesRoutes(app: FastifyInstance) {
|
||||
// 1. Tüm Aktif Şablonları Listele (Mobil ve Web İçin)
|
||||
app.get("/qr-templates", async () => {
|
||||
const list = await loadTemplatesFromDisk();
|
||||
return {
|
||||
templates: list.filter((t) => t.isActive),
|
||||
total: list.length,
|
||||
};
|
||||
});
|
||||
|
||||
// 2. Admin Şablon Listesi (Aktif + Taslak hepsi)
|
||||
app.get("/admin/qr-templates", async () => {
|
||||
const list = await loadTemplatesFromDisk();
|
||||
return {
|
||||
templates: list,
|
||||
total: list.length,
|
||||
};
|
||||
});
|
||||
|
||||
// 3. Yeni Şablon Ekle veya Mevcut Olanı Güncelle
|
||||
app.post<{ Body: Partial<DynamicQrTemplate> }>("/admin/qr-templates", async (req, reply) => {
|
||||
const body = req.body;
|
||||
if (!body.name) {
|
||||
return reply.status(400).send({ error: "Şablon adı zorunludur." });
|
||||
}
|
||||
|
||||
const key = body.key || `qr-${Date.now()}`;
|
||||
const list = await loadTemplatesFromDisk();
|
||||
const existingIndex = list.findIndex((t) => t.id === key || t.key === key);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const newTemplate: DynamicQrTemplate = {
|
||||
key,
|
||||
id: key,
|
||||
name: body.name,
|
||||
categoryName: body.categoryName || "Özel Şablon",
|
||||
svgTemplateUrl: body.svgTemplateUrl || `/templates/${key}.svg`,
|
||||
aspectRatio: body.aspectRatio || "1/1",
|
||||
bgType: body.bgType || "gradient",
|
||||
bgImageUrl: body.bgImageUrl,
|
||||
bgGradient: body.bgGradient || "radial-gradient(circle at 50% 50%, #132034 0%, #0b0d13 100%)",
|
||||
plaqueColor: body.plaqueColor || "#132034",
|
||||
borderAccent: body.borderAccent || "#D4AF37",
|
||||
accentText: body.accentText || "#D4AF37",
|
||||
headerText: body.headerText || "#FFFFFF",
|
||||
subText: body.subText || "#E5C378",
|
||||
dividerColor: body.dividerColor || "rgba(212, 175, 55, 0.3)",
|
||||
isActive: body.isActive !== undefined ? body.isActive : true,
|
||||
createdAt: existingIndex >= 0 ? list[existingIndex].createdAt : now,
|
||||
updatedAt: now,
|
||||
defaultQrConfig: body.defaultQrConfig || (body as any).qrConfig || {
|
||||
xPercent: 50,
|
||||
yPercent: 38,
|
||||
size: 115,
|
||||
borderRadius: 14,
|
||||
borderColor: body.borderAccent || "#D4AF37",
|
||||
bgBoxColor: "#FFFFFF",
|
||||
},
|
||||
defaultLayers: body.defaultLayers || (body as any).layers || [
|
||||
{
|
||||
id: "rName",
|
||||
text: "{{RESTAURANT_NAME}}",
|
||||
yPercent: 16,
|
||||
fontSize: 18,
|
||||
color: body.accentText || "#D4AF37",
|
||||
fontWeight: "800",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = newTemplate;
|
||||
} else {
|
||||
list.unshift(newTemplate);
|
||||
}
|
||||
|
||||
await saveTemplatesToDisk(list);
|
||||
return { success: true, template: newTemplate };
|
||||
});
|
||||
|
||||
// 4. Şablon Sil
|
||||
app.delete<{ Params: { id: string } }>("/admin/qr-templates/:id", async (req, reply) => {
|
||||
const { id } = req.params;
|
||||
const list = await loadTemplatesFromDisk();
|
||||
const filtered = list.filter((t) => t.id !== id && t.key !== id);
|
||||
|
||||
if (filtered.length === list.length) {
|
||||
return reply.status(404).send({ error: "Şablon bulunamadı." });
|
||||
}
|
||||
|
||||
await saveTemplatesToDisk(filtered);
|
||||
return { success: true, deletedId: id };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user