From 20b254e614ec2c164cebfa5ef0b3e97daf32a36f Mon Sep 17 00:00:00 2001 From: Ksan Date: Sat, 1 Aug 2026 16:07:57 +0200 Subject: [PATCH] Initial commit --- .gitignore | 44 + AGENTS.md | 5 + CLAUDE.md | 1 + README.md | 36 + app/[locale]/[...rest]/page.tsx | 8 + app/[locale]/about/AboutContent.tsx | 207 ++ app/[locale]/about/page.tsx | 37 + app/[locale]/contact/page.tsx | 68 + app/[locale]/layout.tsx | 131 + app/[locale]/not-found.tsx | 43 + app/[locale]/opengraph-image.tsx | 74 + app/[locale]/page.tsx | 25 + app/[locale]/plans/[code]/PlansContent.tsx | 224 ++ app/[locale]/plans/[code]/page.tsx | 53 + app/[locale]/plans/[code]/pricing.ts | 249 ++ app/[locale]/projects/page.tsx | 77 + app/[locale]/services/ServicesContent.tsx | 381 +++ app/[locale]/services/page.tsx | 37 + app/api/contact/route.ts | 225 ++ app/favicon.ico | Bin 0 -> 15086 bytes app/globals.css | 225 ++ app/icon.svg | 12 + app/robots.ts | 16 + app/sitemap.ts | 40 + components/layout/Footer.tsx | 125 + components/layout/Navbar.tsx | 188 ++ components/sections/CTA.tsx | 77 + components/sections/ContactForm.tsx | 292 +++ components/sections/Hero.tsx | 138 + components/sections/ProjectsCarousel.tsx | 517 ++++ components/sections/ProjectsShowcase.tsx | 171 ++ components/sections/Services.tsx | 98 + components/sections/TechStack.tsx | 146 ++ components/ui/GlassCard.tsx | 32 + components/ui/GlowButton.tsx | 94 + components/ui/IPInfoCard.tsx | 215 ++ components/ui/LangBanner.tsx | 97 + components/ui/LanguageSwitcher.tsx | 90 + components/ui/Logo.tsx | 41 + components/ui/ProjectModal.tsx | 298 +++ components/ui/ScanlineOverlay.tsx | 31 + design/hero-layouts/idea1.png | Bin 0 -> 199590 bytes design/hero-layouts/idea2.png | Bin 0 -> 146831 bytes design/hero-layouts/idea3.png | Bin 0 -> 205888 bytes design/hero-layouts/idea4.png | Bin 0 -> 191070 bytes design/hero-layouts/idea5.png | Bin 0 -> 273094 bytes eslint.config.mjs | 18 + lib/animations.ts | 42 + lib/dictionaries.ts | 503 ++++ lib/seo.ts | 17 + lib/utils.ts | 12 + next.config.ts | 7 + package-lock.json | 2771 ++++++++++++++++++++ package.json | 34 + postcss.config.mjs | 7 + proxy.ts | 66 + public/botinator.png | Bin 0 -> 1457545 bytes public/etf1.png | Bin 0 -> 270923 bytes public/etf2.png | Bin 0 -> 579444 bytes public/etfoglasi.png | Bin 0 -> 237735 bytes public/etfoglasiapp.jpg | Bin 0 -> 331553 bytes public/evoting.png | Bin 0 -> 1244299 bytes public/kd.svg | 88 + public/site1.png | Bin 0 -> 101519 bytes public/site2.png | Bin 0 -> 314756 bytes public/sshksan.png | Bin 0 -> 1006020 bytes public/tba.png | Bin 0 -> 1426719 bytes tsconfig.json | 34 + 68 files changed, 8467 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 app/[locale]/[...rest]/page.tsx create mode 100644 app/[locale]/about/AboutContent.tsx create mode 100644 app/[locale]/about/page.tsx create mode 100644 app/[locale]/contact/page.tsx create mode 100644 app/[locale]/layout.tsx create mode 100644 app/[locale]/not-found.tsx create mode 100644 app/[locale]/opengraph-image.tsx create mode 100644 app/[locale]/page.tsx create mode 100644 app/[locale]/plans/[code]/PlansContent.tsx create mode 100644 app/[locale]/plans/[code]/page.tsx create mode 100644 app/[locale]/plans/[code]/pricing.ts create mode 100644 app/[locale]/projects/page.tsx create mode 100644 app/[locale]/services/ServicesContent.tsx create mode 100644 app/[locale]/services/page.tsx create mode 100644 app/api/contact/route.ts create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/icon.svg create mode 100644 app/robots.ts create mode 100644 app/sitemap.ts create mode 100644 components/layout/Footer.tsx create mode 100644 components/layout/Navbar.tsx create mode 100644 components/sections/CTA.tsx create mode 100644 components/sections/ContactForm.tsx create mode 100644 components/sections/Hero.tsx create mode 100644 components/sections/ProjectsCarousel.tsx create mode 100644 components/sections/ProjectsShowcase.tsx create mode 100644 components/sections/Services.tsx create mode 100644 components/sections/TechStack.tsx create mode 100644 components/ui/GlassCard.tsx create mode 100644 components/ui/GlowButton.tsx create mode 100644 components/ui/IPInfoCard.tsx create mode 100644 components/ui/LangBanner.tsx create mode 100644 components/ui/LanguageSwitcher.tsx create mode 100644 components/ui/Logo.tsx create mode 100644 components/ui/ProjectModal.tsx create mode 100644 components/ui/ScanlineOverlay.tsx create mode 100644 design/hero-layouts/idea1.png create mode 100644 design/hero-layouts/idea2.png create mode 100644 design/hero-layouts/idea3.png create mode 100644 design/hero-layouts/idea4.png create mode 100644 design/hero-layouts/idea5.png create mode 100644 eslint.config.mjs create mode 100644 lib/animations.ts create mode 100644 lib/dictionaries.ts create mode 100644 lib/seo.ts create mode 100644 lib/utils.ts create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 proxy.ts create mode 100644 public/botinator.png create mode 100644 public/etf1.png create mode 100644 public/etf2.png create mode 100644 public/etfoglasi.png create mode 100644 public/etfoglasiapp.jpg create mode 100644 public/evoting.png create mode 100644 public/kd.svg create mode 100644 public/site1.png create mode 100644 public/site2.png create mode 100644 public/sshksan.png create mode 100644 public/tba.png create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d6e478 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# IDE +.idea/ + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8bd0e39 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/[locale]/[...rest]/page.tsx b/app/[locale]/[...rest]/page.tsx new file mode 100644 index 0000000..eed5d8a --- /dev/null +++ b/app/[locale]/[...rest]/page.tsx @@ -0,0 +1,8 @@ +import { notFound } from "next/navigation"; + +// Catch-all for unknown paths under a locale (e.g. /en/asdf) so they render +// the styled not-found page inside the locale layout instead of the bare +// default 404. +export default function CatchAllPage() { + notFound(); +} diff --git a/app/[locale]/about/AboutContent.tsx b/app/[locale]/about/AboutContent.tsx new file mode 100644 index 0000000..2da144b --- /dev/null +++ b/app/[locale]/about/AboutContent.tsx @@ -0,0 +1,207 @@ +"use client"; + +import Link from "next/link"; +import { motion } from "framer-motion"; +import { + GitBranch, + Lock, + Zap, + Sparkles, + MapPin, + Server, + Globe, + Briefcase, + Clock, +} from "lucide-react"; +import GlassCard from "@/components/ui/GlassCard"; +import { fadeUp, stagger, viewportConfig } from "@/lib/animations"; +import type { Dictionary, Locale } from "@/lib/dictionaries"; + +const highlightIcons = [GitBranch, Lock, Zap, Sparkles]; + +const factIcons = [MapPin, Server, Globe, Briefcase, Clock]; + +interface AboutContentProps { + locale: Locale; + dict: Dictionary; +} + +interface TextParagraph { + type: "text"; + content: string; +} + +interface LinkParagraph { + type: "linkParagraph"; + before: string; + linkText: string; + href: string; +} + +type ApproachParagraph = TextParagraph | LinkParagraph; + +export default function AboutContent({ locale, dict }: AboutContentProps) { + const t = dict.aboutPage; + + return ( + <> + {/* Hero */} +
+
+ +
+
+ + {t.badge} + + + + {t.title} +
+ {t.titleAccent} +
+ + + {t.subtitle} + +
+
+
+ + {/* Approach + Quick facts */} +
+
+ + {/* Main text */} + +

+ {t.approachTitle} +

+
+ {t.approachParagraphs.map((p : ApproachParagraph, i : number) => { + if (p.type === "text") { + return

{p.content}

; + } + + if (p.type === "linkParagraph") { + return ( +

+ {p.before} + + {p.linkText} + + . +

+ ); + } + + return null; + })} +
+
+ + {/* Quick facts */} + + +

+ {t.quickFactsTitle} +

+
+ {t.facts.map((fact, i) => { + const Icon = factIcons[i] || MapPin; + return ( +
+
+ + {fact.label} +
+
{fact.value}
+
+ ); + })} +
+
+
+
+
+
+ + {/* Highlights */} +
+
+ + + {t.highlightsTitle} + + + + + {t.highlights.map((item, i) => { + const Icon = highlightIcons[i] || Sparkles; + return ( + + +
+
+ +
+
+

+ {item.title} +

+

+ {item.description} +

+
+
+
+
+ ); + })} +
+
+
+ + ); +} diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx new file mode 100644 index 0000000..c0f19fe --- /dev/null +++ b/app/[locale]/about/page.tsx @@ -0,0 +1,37 @@ +import { getDictionary, type Locale } from "@/lib/dictionaries"; +import CTA from "@/components/sections/CTA"; +import AboutContent from "./AboutContent"; +import { localeAlternates } from "@/lib/seo"; +import type { Metadata } from "next"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + const isSR = locale === "sr"; + return { + title: isSR ? "O meni" : "About", + description: isSR + ? "Full-stack inženjer koji gradi, deployuje i hostuje na vlastitoj infrastrukturi." + : "End-to-end engineer who builds, deploys, and hosts on self-managed infrastructure.", + alternates: localeAlternates(locale, "/about"), + }; +} + +export default async function AboutPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const dict = getDictionary(locale); + + return ( + <> + + + + ); +} diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx new file mode 100644 index 0000000..ff2590c --- /dev/null +++ b/app/[locale]/contact/page.tsx @@ -0,0 +1,68 @@ +import { getDictionary } from "@/lib/dictionaries"; +import ContactForm from "@/components/sections/ContactForm"; +import { localeAlternates } from "@/lib/seo"; +import type { Metadata } from "next"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + const isSR = locale === "sr"; + return { + title: isSR ? "Kontakt" : "Contact", + description: isSR + ? "Kontaktirajte me za vaš sljedeći projekat." + : "Get in touch about your next project.", + alternates: localeAlternates(locale, "/contact"), + }; +} + +export default async function ContactPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const dict = getDictionary(locale); + const t = dict.contactPage; + + return ( + <> + {/* Hero */} +
+
+ +
+
+ + + + + + {t.badge} + + +

+ {t.title} +
+ {t.titleAccent} +

+ +

+ {t.subtitle} +

+
+
+
+ + {/* Form section */} +
+
+ +
+
+ + ); +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..6274409 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,131 @@ +import type { Metadata, Viewport } from "next"; +import { Geist, Sora, JetBrains_Mono } from "next/font/google"; +import "../globals.css"; +import { + locales, + getDictionary, + isValidLocale, + defaultLocale, + type Locale, +} from "@/lib/dictionaries"; +import { SITE_URL, localeAlternates } from "@/lib/seo"; +import Navbar from "@/components/layout/Navbar"; +import Footer from "@/components/layout/Footer"; +import ScanlineOverlay from "@/components/ui/ScanlineOverlay"; +import LangBanner from "@/components/ui/LangBanner"; + +const geistSans = Geist({ + subsets: ["latin"], + variable: "--font-geist-sans", + display: "swap", +}); + +const sora = Sora({ + subsets: ["latin"], + variable: "--font-sora", + display: "swap", + weight: ["300", "400", "500", "600", "700"], +}); + +const jetbrains = JetBrains_Mono({ + subsets: ["latin"], + variable: "--font-jetbrains", + display: "swap", + weight: ["400", "500"], +}); + +// Pre-render both locale shells at build time +export function generateStaticParams() { + return locales.map((locale) => ({ locale })); +} + +// Tell the browser the document is dark before any CSS/JS runs. This sets the +// / theme-color in the SSR so the initial +// canvas (and mobile browser chrome) paints dark, never white. +export const viewport: Viewport = { + colorScheme: "dark", + themeColor: "#0F1117", +}; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + const isSR = locale === "sr"; + + return { + title: { + default: isSR + ? "Name — Full-stack inženjering, od početka do kraja" + : "Name — Full-stack engineering, end to end", + template: "%s · Name", + }, + description: isSR + ? "Premium full-stack razvoj, deployment i self-hosted infrastruktura." + : "Premium full-stack development, deployment, and self-hosted infrastructure.", + metadataBase: new URL(SITE_URL), + + // Canonical — self-referencing within each locale. + // Subpages override this with their own path via localeAlternates(). + alternates: localeAlternates(locale), + + openGraph: { + title: isSR + ? "Name — Full-stack inženjering" + : "Name — Full-stack engineering, end to end", + description: isSR + ? "Premium full-stack razvoj i self-hosted infrastruktura." + : "Premium full-stack development and self-hosted infrastructure.", + type: "website", + locale: isSR ? "sr_RS" : "en_US", + alternateLocale: isSR ? ["en_US"] : ["sr_RS"], + siteName: "ksan.dev", + url: `${SITE_URL}/${locale}`, + }, + + twitter: { + card: "summary_large_image", + title: isSR ? "Name — Full-stack inženjering" : "Name — Full-stack engineering", + }, + + robots: { + index: true, + follow: true, + "max-image-preview": "large", + "max-snippet": -1, + }, + }; +} + +export default async function LocaleLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}) { + const { locale: rawLocale } = await params; + const locale: Locale = isValidLocale(rawLocale) ? rawLocale : defaultLocale; + const dict = getDictionary(locale); + + return ( + + + + + +
{children}
+