diff --git a/public/brand/.gitkeep b/public/brand/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/public/brand/icon.png b/public/brand/icon.png new file mode 100644 index 0000000..b83858d Binary files /dev/null and b/public/brand/icon.png differ diff --git a/public/brand/logo-dark.svg b/public/brand/logo-dark.svg new file mode 100644 index 0000000..700c969 --- /dev/null +++ b/public/brand/logo-dark.svg @@ -0,0 +1,5 @@ + + + + BÜTÇEM + diff --git a/public/brand/logo.svg b/public/brand/logo.svg new file mode 100644 index 0000000..60fd7b4 --- /dev/null +++ b/public/brand/logo.svg @@ -0,0 +1,5 @@ + + + + BÜTÇEM + diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..0147bc1 Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..faf8265 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/icons/maskable-512.png b/public/icons/maskable-512.png new file mode 100644 index 0000000..609e48e Binary files /dev/null and b/public/icons/maskable-512.png differ diff --git a/scripts/generate-icons.ts b/scripts/generate-icons.ts new file mode 100644 index 0000000..c55f90d --- /dev/null +++ b/scripts/generate-icons.ts @@ -0,0 +1,90 @@ +/** + * Uygulama ikonlarını logodan üretir: npm run icons + * + * Kaynak: public/brand/icon.png (kare, en az 512×512). + * Çıktılar (commit edilir): + * src/app/favicon.ico 16/32/48 px (PNG gömülü ICO) + * src/app/icon.png 192 px → + * src/app/apple-icon.png 180 px, saydamsız → iOS ana ekran + * public/icons/icon-192.png PWA manifest + * public/icons/icon-512.png PWA manifest + * public/icons/maskable-512.png PWA manifest (purpose: maskable, %80 güvenli alan) + * + * Logoyu değiştirdikten sonra bu betiği yeniden çalıştırın. + */ +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import sharp from "sharp"; + +const ROOT = process.cwd(); +const SOURCE = path.join(ROOT, "public/brand/icon.png"); +/** Saydamlık kabul etmeyen ikonlarda arka plan: marka mavisi (--primary, açık tema). */ +const BRAND_BG = "#105bbe"; + +async function png(size: number, opts: { flatten?: boolean; padding?: number } = {}) { + const inner = Math.round(size * (1 - (opts.padding ?? 0) * 2)); + const resized = await sharp(SOURCE).resize(inner, inner, { fit: "contain" }).png().toBuffer(); + let img = sharp({ + create: { + width: size, + height: size, + channels: 4, + background: opts.flatten || opts.padding ? BRAND_BG : { r: 0, g: 0, b: 0, alpha: 0 }, + }, + }).composite([{ input: resized, gravity: "center" }]); + if (opts.flatten) img = img.flatten({ background: BRAND_BG }); + return img.png({ compressionLevel: 9 }).toBuffer(); +} + +/** PNG görüntülerinden ICO dosyası oluşturur (Vista+ tüm tarayıcılar PNG gömülü ICO destekler). */ +function toIco(images: { size: number; data: Buffer }[]): Buffer { + const header = Buffer.alloc(6); + header.writeUInt16LE(0, 0); // reserved + header.writeUInt16LE(1, 2); // type: icon + header.writeUInt16LE(images.length, 4); + const entries: Buffer[] = []; + let offset = 6 + images.length * 16; + for (const { size, data } of images) { + const e = Buffer.alloc(16); + e.writeUInt8(size >= 256 ? 0 : size, 0); + e.writeUInt8(size >= 256 ? 0 : size, 1); + e.writeUInt8(0, 2); // palette + e.writeUInt8(0, 3); // reserved + e.writeUInt16LE(1, 4); // color planes + e.writeUInt16LE(32, 6); // bpp + e.writeUInt32LE(data.length, 8); + e.writeUInt32LE(offset, 12); + offset += data.length; + entries.push(e); + } + return Buffer.concat([header, ...entries, ...images.map((i) => i.data)]); +} + +async function main() { + const meta = await sharp(SOURCE).metadata(); + if (!meta.width || !meta.height || meta.width !== meta.height || meta.width < 512) { + throw new Error(`${SOURCE} kare ve en az 512×512 olmalı (şu an ${meta.width}×${meta.height}).`); + } + await mkdir(path.join(ROOT, "public/icons"), { recursive: true }); + + const out: [string, Buffer][] = [ + ["src/app/icon.png", await png(192)], + ["src/app/apple-icon.png", await png(180, { flatten: true })], + ["public/icons/icon-192.png", await png(192)], + ["public/icons/icon-512.png", await png(512)], + ["public/icons/maskable-512.png", await png(512, { padding: 0.1 })], + [ + "src/app/favicon.ico", + toIco(await Promise.all([16, 32, 48].map(async (size) => ({ size, data: await png(size) })))), + ], + ]; + for (const [file, data] of out) { + await writeFile(path.join(ROOT, file), data); + console.log(`✓ ${file} (${data.length} bayt)`); + } +} + +main().catch((err: unknown) => { + console.error(err instanceof Error ? err.message : err); + process.exit(1); +}); diff --git a/src/app/apple-icon.png b/src/app/apple-icon.png new file mode 100644 index 0000000..d442934 Binary files /dev/null and b/src/app/apple-icon.png differ diff --git a/src/app/favicon.ico b/src/app/favicon.ico index af98450..fbab8dd 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/globals.css b/src/app/globals.css index 6a25b1b..ae2ff36 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -18,6 +18,14 @@ --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar: var(--sidebar); + --color-income: var(--income); + --color-income-foreground: var(--income-foreground); + --color-expense: var(--expense); + --color-expense-foreground: var(--expense-foreground); + --color-budget: var(--budget); + --color-budget-foreground: var(--budget-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); --color-chart-5: var(--chart-5); --color-chart-4: var(--chart-4); --color-chart-3: var(--chart-3); @@ -48,73 +56,98 @@ --radius-4xl: calc(var(--radius) * 2.6); } +/* + * BÜTÇEM marka paleti — mavi ve beyaz tonları (oklch). Sabit renk kodu yalnızca burada. + * Anlamsal renkler: --income (gelir/yeşil), --expense (gider/kırmızı), + * --budget (bütçe-plan/mavi), --warning (uyarı/turuncu). Grafikler de bunları kullanır. + */ :root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); + --background: oklch(0.99 0.004 250); + --foreground: oklch(0.22 0.04 262); --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); + --card-foreground: oklch(0.22 0.04 262); --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); + --popover-foreground: oklch(0.22 0.04 262); + --primary: oklch(0.49 0.17 258); + --primary-foreground: oklch(0.99 0.004 250); + --secondary: oklch(0.955 0.016 250); + --secondary-foreground: oklch(0.3 0.06 260); + --muted: oklch(0.955 0.012 250); + --muted-foreground: oklch(0.5 0.03 258); + --accent: oklch(0.935 0.03 250); + --accent-foreground: oklch(0.3 0.08 260); + --destructive: oklch(0.56 0.21 25); + --border: oklch(0.905 0.016 252); + --input: oklch(0.89 0.018 252); + --ring: oklch(0.6 0.15 258); + + --income: oklch(0.58 0.15 152); + --income-foreground: oklch(0.99 0.004 250); + --expense: oklch(0.56 0.21 25); + --expense-foreground: oklch(0.99 0.004 250); + --budget: oklch(0.55 0.17 258); + --budget-foreground: oklch(0.99 0.004 250); + --warning: oklch(0.68 0.17 55); + --warning-foreground: oklch(0.22 0.04 262); + + --chart-1: var(--income); + --chart-2: var(--budget); + --chart-3: var(--expense); + --chart-4: var(--warning); + --chart-5: oklch(0.65 0.08 230); --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --sidebar: oklch(0.975 0.008 250); + --sidebar-foreground: oklch(0.22 0.04 262); + --sidebar-primary: oklch(0.49 0.17 258); + --sidebar-primary-foreground: oklch(0.99 0.004 250); + --sidebar-accent: oklch(0.935 0.03 250); + --sidebar-accent-foreground: oklch(0.3 0.08 260); + --sidebar-border: oklch(0.905 0.016 252); + --sidebar-ring: oklch(0.6 0.15 258); } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --background: oklch(0.18 0.04 264); + --foreground: oklch(0.96 0.01 250); + --card: oklch(0.22 0.045 264); + --card-foreground: oklch(0.96 0.01 250); + --popover: oklch(0.22 0.045 264); + --popover-foreground: oklch(0.96 0.01 250); + --primary: oklch(0.76 0.12 245); + --primary-foreground: oklch(0.18 0.04 264); + --secondary: oklch(0.27 0.045 264); + --secondary-foreground: oklch(0.96 0.01 250); + --muted: oklch(0.26 0.04 264); + --muted-foreground: oklch(0.73 0.03 250); + --accent: oklch(0.3 0.06 262); + --accent-foreground: oklch(0.96 0.01 250); + --destructive: oklch(0.7 0.18 22); + --border: oklch(1 0 0 / 11%); + --input: oklch(1 0 0 / 16%); + --ring: oklch(0.7 0.12 245); + + --income: oklch(0.75 0.16 152); + --income-foreground: oklch(0.18 0.04 264); + --expense: oklch(0.7 0.18 22); + --expense-foreground: oklch(0.18 0.04 264); + --budget: oklch(0.73 0.13 245); + --budget-foreground: oklch(0.18 0.04 264); + --warning: oklch(0.79 0.15 62); + --warning-foreground: oklch(0.18 0.04 264); + + --chart-1: var(--income); + --chart-2: var(--budget); + --chart-3: var(--expense); + --chart-4: var(--warning); + --chart-5: oklch(0.7 0.07 230); + --sidebar: oklch(0.2 0.045 264); + --sidebar-foreground: oklch(0.96 0.01 250); + --sidebar-primary: oklch(0.76 0.12 245); + --sidebar-primary-foreground: oklch(0.18 0.04 264); + --sidebar-accent: oklch(0.28 0.055 262); + --sidebar-accent-foreground: oklch(0.96 0.01 250); + --sidebar-border: oklch(1 0 0 / 11%); + --sidebar-ring: oklch(0.7 0.12 245); } @layer base { @@ -128,3 +161,8 @@ @apply font-sans; } } + +/* Mobil alt menü + iOS ana ekran çentiği için güvenli alan */ +@utility pb-safe { + padding-bottom: env(safe-area-inset-bottom); +} diff --git a/src/app/icon.png b/src/app/icon.png new file mode 100644 index 0000000..0147bc1 Binary files /dev/null and b/src/app/icon.png differ diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 0000000..6e4962f --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,26 @@ +import type { MetadataRoute } from "next"; +import { tr } from "@/i18n/tr"; + +/** PWA manifest: "Ana ekrana ekle" ile kurulabilir. İkonlar: npm run icons. */ +export default function manifest(): MetadataRoute.Manifest { + return { + id: "/", + name: tr.app.name, + short_name: tr.app.name, + description: tr.app.tagline, + lang: "tr-TR", + dir: "ltr", + start_url: "/dashboard", + scope: "/", + display: "standalone", + orientation: "portrait", + background_color: "#fafcfe", + theme_color: "#105bbe", + categories: ["finance", "productivity"], + icons: [ + { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any" }, + { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" }, + { src: "/icons/maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" }, + ], + }; +} diff --git a/src/components/brand/logo.tsx b/src/components/brand/logo.tsx new file mode 100644 index 0000000..d544b6c --- /dev/null +++ b/src/components/brand/logo.tsx @@ -0,0 +1,27 @@ +import Image from "next/image"; +import { cn } from "@/lib/utils"; +import { tr } from "@/i18n/tr"; + +/** + * Marka logosu: açık temada logo.svg, koyu temada logo-dark.svg (CSS ile seçilir, + * hydration uyumsuzluğu olmaz). Dosyalar public/brand/ altında. + */ +export function Logo({ className, priority }: { className?: string; priority?: boolean }) { + const common = { width: 120, height: 32, priority, unoptimized: true } as const; + return ( + + {tr.app.name} + + + ); +}