commit 243a182d33a8b3d171a1d7152867e66bd70a16fd Author: diyaa Date: Fri Mar 13 03:16:25 2026 +0100 first udpate diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..4011a0d Binary files /dev/null and b/.DS_Store differ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7c85348 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi + +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 +RUN addgroup -S nextjs && adduser -S nextjs -G nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +USER nextjs +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8ecfece --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +SHELL := /bin/zsh +COMPOSE_FILE := docker-compose.yml +PROJECT_NAME := diyaa + +.PHONY: start stop restart logs ps build ensure-env ensure-project + +ensure-env: + @if [ ! -f .env ]; then \ + if [ -f .env.example ]; then \ + cp .env.example .env; \ + echo ".env created from .env.example"; \ + else \ + echo ".env file is missing and .env.example was not found."; \ + exit 1; \ + fi; \ + fi + +ensure-project: ensure-env + @grep -q '^COMPOSE_PROJECT_NAME=$(PROJECT_NAME)$$' .env || \ + (echo "COMPOSE_PROJECT_NAME in .env must be '$(PROJECT_NAME)' to avoid project mix-up."; exit 1) + +start: ensure-project + docker compose --env-file .env -f $(COMPOSE_FILE) up -d --build + @echo "diyaa services are up" + +stop: ensure-project + docker compose --env-file .env -f $(COMPOSE_FILE) down + +restart: stop start + +logs: ensure-project + docker compose --env-file .env -f $(COMPOSE_FILE) logs -f --tail=200 + +ps: ensure-project + docker compose --env-file .env -f $(COMPOSE_FILE) ps + +build: ensure-project + docker compose --env-file .env -f $(COMPOSE_FILE) build diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 0000000..9718a94 Binary files /dev/null and b/app/.DS_Store differ diff --git a/app/[locale]/.DS_Store b/app/[locale]/.DS_Store new file mode 100644 index 0000000..597fc03 Binary files /dev/null and b/app/[locale]/.DS_Store differ diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx new file mode 100644 index 0000000..0c170f9 --- /dev/null +++ b/app/[locale]/about/page.tsx @@ -0,0 +1,39 @@ +import { getDictionary, isLocale, type Locale } from "@/lib/i18n"; +import { notFound } from "next/navigation"; + +export default function AboutPage({ params }: { params: { locale: string } }) { + if (!isLocale(params.locale)) { + notFound(); + } + + const locale = params.locale as Locale; + const dictionary = getDictionary(locale); + + return ( +
+

{dictionary.about.kicker}

+

{dictionary.about.title}

+

{dictionary.about.story}

+ +
+
+

{dictionary.about.skillsTitle}

+
    + {dictionary.about.skills.map((skill) => ( +
  • {skill}
  • + ))} +
+
+ +
+

{dictionary.about.experienceTitle}

+
    + {dictionary.about.experience.map((item) => ( +
  • {item}
  • + ))} +
+
+
+
+ ); +} diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx new file mode 100644 index 0000000..3b27d2a --- /dev/null +++ b/app/[locale]/contact/page.tsx @@ -0,0 +1,31 @@ +import { getDictionary, isLocale, type Locale } from "@/lib/i18n"; +import { notFound } from "next/navigation"; + +export default function ContactPage({ params }: { params: { locale: string } }) { + if (!isLocale(params.locale)) { + notFound(); + } + + const locale = params.locale as Locale; + const dictionary = getDictionary(locale); + + return ( +
+

{dictionary.contact.kicker}

+

{dictionary.contact.title}

+

{dictionary.contact.description}

+ +
+ {dictionary.contact.channels.map((channel) => ( +
+

{channel.name}

+

{channel.status}

+ +
+ ))} +
+
+ ); +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..5198160 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from "react"; +import { notFound } from "next/navigation"; +import BottomNav from "@/components/BottomNav"; +import SiteFooter from "@/components/SiteFooter"; +import SiteHeader from "@/components/SiteHeader"; +import { getDictionary, getDirection, isLocale, locales, type Locale } from "@/lib/i18n"; + +export function generateStaticParams() { + return locales.map((locale) => ({ locale })); +} + +export default function LocaleLayout({ + children, + params, +}: { + children: ReactNode; + params: { locale: string }; +}) { + if (!isLocale(params.locale)) { + notFound(); + } + + const locale = params.locale as Locale; + const dictionary = getDictionary(locale); + + return ( +
+ +
{children}
+ + +
+ ); +} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx new file mode 100644 index 0000000..53bfdb3 --- /dev/null +++ b/app/[locale]/page.tsx @@ -0,0 +1,14 @@ +import HeroSection from "@/components/HeroSection"; +import { getDictionary, isLocale, type Locale } from "@/lib/i18n"; +import { notFound } from "next/navigation"; + +export default function HomePage({ params }: { params: { locale: string } }) { + if (!isLocale(params.locale)) { + notFound(); + } + + const locale = params.locale as Locale; + const dictionary = getDictionary(locale); + + return ; +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..944dec3 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,413 @@ +:root { + --bg: #07111f; + --surface: rgba(11, 20, 36, 0.84); + --surface-2: rgba(16, 28, 48, 0.78); + --surface-3: rgba(23, 38, 64, 0.92); + --text: #eef4ff; + --muted: #9fb1cc; + --line: rgba(180, 201, 232, 0.18); + --line-strong: rgba(202, 219, 245, 0.3); + --brand: #ff7a59; + --brand-2: #5ab2ff; + --brand-3: #f4c76b; + --button-secondary: rgba(22, 35, 58, 0.9); + --button-secondary-hover: rgba(28, 45, 72, 0.96); + --button-disabled: rgba(17, 28, 46, 0.72); + --card-bg: rgba(16, 28, 48, 0.82); + --nav-active-bg: rgba(238, 244, 255, 0.94); + --nav-active-text: #0d1728; + --shadow: 0 34px 70px rgba(2, 7, 20, 0.58); +} + +[data-theme="light"] { + --bg: #f5f7ff; + --surface: rgba(255, 255, 255, 0.82); + --surface-2: rgba(255, 255, 255, 0.7); + --surface-3: rgba(255, 255, 255, 0.92); + --text: #0e1320; + --muted: #475069; + --line: rgba(14, 19, 32, 0.12); + --line-strong: rgba(14, 19, 32, 0.18); + --brand: #ff5d47; + --brand-2: #0d8bff; + --brand-3: #ffbf3f; + --button-secondary: rgba(255, 255, 255, 0.72); + --button-secondary-hover: rgba(255, 255, 255, 0.92); + --button-disabled: rgba(255, 255, 255, 0.7); + --card-bg: rgba(255, 255, 255, 0.74); + --nav-active-bg: #ffffff; + --nav-active-text: #111827; + --shadow: 0 30px 60px rgba(13, 22, 49, 0.2); +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +body { + min-height: 100vh; + color: var(--text); + background: + radial-gradient(circle at 12% 16%, color-mix(in srgb, var(--brand) 24%, transparent), transparent 31%), + radial-gradient(circle at 86% 12%, color-mix(in srgb, var(--brand-2) 22%, transparent), transparent 30%), + radial-gradient(circle at 84% 84%, color-mix(in srgb, var(--brand-3) 18%, transparent), transparent 33%), + linear-gradient(180deg, color-mix(in srgb, var(--surface) 32%, var(--bg)) 0%, var(--bg) 52%, color-mix(in srgb, var(--surface-2) 20%, var(--bg)) 100%), + var(--bg); + font-family: "Space Grotesk", "IBM Plex Sans Arabic", "Cairo", sans-serif; + line-height: 1.6; +} + +a { + color: inherit; + text-decoration: none; +} + +.site-shell { + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr auto; +} + +.container { + width: min(1120px, 92vw); + margin: 0 auto; +} + +.site-header { + position: sticky; + top: 0; + z-index: 20; + backdrop-filter: blur(18px); + background: color-mix(in srgb, var(--bg) 76%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--line) 86%, transparent); +} + +.bar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1.2rem; + padding: 0.9rem 0; +} + +.brand { + font-size: 1.05rem; + font-weight: 800; + letter-spacing: 0.04em; +} + +.theme-toggle { + border: 1px solid var(--line); + background: color-mix(in srgb, var(--surface-2) 88%, transparent); + color: var(--text); + border-radius: 999px; + padding: 0.4rem 0.8rem; + font: inherit; + font-size: 0.82rem; + font-weight: 700; + cursor: pointer; + transition: transform 180ms ease, border-color 180ms ease; +} + +.theme-toggle:hover, +.theme-toggle:focus-visible { + border-color: color-mix(in srgb, var(--brand-2) 50%, var(--line)); + transform: translateY(-1px); + outline: none; +} + +.theme-toggle-label { + min-width: 3.1rem; + display: inline-flex; + justify-content: center; +} + +.language-switcher { + display: inline-flex; + gap: 0.4rem; + padding: 0.2rem; + border-radius: 999px; + background: color-mix(in srgb, var(--surface-2) 86%, transparent); + border: 1px solid color-mix(in srgb, var(--line-strong) 86%, transparent); +} + +.lang-chip { + padding: 0.35rem 0.65rem; + border-radius: 999px; + font-size: 0.8rem; + font-weight: 700; +} + +.lang-chip.active { + color: white; + background: linear-gradient(90deg, var(--brand), var(--brand-2)); +} + +.page-content { + width: min(1120px, 92vw); + margin: 2rem auto 2rem; + padding-bottom: 1.1rem; + animation: fade-up 460ms ease both; +} + +.panel { + background: var(--surface); + border: 1px solid var(--line-strong); + border-radius: 24px; + box-shadow: var(--shadow); + padding: clamp(1.3rem, 2vw, 2rem); +} + +.hero h1, +.panel h1 { + font-size: clamp(1.8rem, 5vw, 3.3rem); + line-height: 1.12; + margin: 0.2rem 0 0.9rem; +} + +.eyebrow { + margin: 0; + font-size: 0.83rem; + letter-spacing: 0.14em; + text-transform: uppercase; + font-weight: 700; + color: var(--brand-2); +} + +.lead { + margin: 0; + color: var(--muted); + max-width: 70ch; +} + +.cta-row { + margin-top: 1.4rem; + display: flex; + gap: 0.8rem; + flex-wrap: wrap; +} + +.cta-btn, +.ghost-btn, +.pending-btn { + border-radius: 12px; + border: 1px solid transparent; + padding: 0.74rem 1rem; + font-size: 0.95rem; + font-weight: 700; +} + +.cta-btn { + color: white; + background: linear-gradient(115deg, var(--brand), var(--brand-2)); +} + +.ghost-btn { + border-color: var(--line); + background: var(--button-secondary); +} + +.pending-btn { + margin-top: 0.6rem; + border-color: var(--line); + color: var(--muted); + background: var(--button-disabled); + cursor: not-allowed; +} + +.cta-btn:hover, +.ghost-btn:hover, +.cta-btn:focus-visible, +.ghost-btn:focus-visible { + transform: translateY(-1px); + outline: none; +} + +.ghost-btn:hover, +.ghost-btn:focus-visible { + background: var(--button-secondary-hover); + border-color: color-mix(in srgb, var(--brand-2) 24%, var(--line-strong)); +} + +.stat-grid, +.split-grid { + margin-top: 1.5rem; + display: grid; + gap: 1rem; +} + +.stat-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.split-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.stat-card, +.card { + border-radius: 18px; + border: 1px solid var(--line); + background: var(--card-bg); + padding: 1rem; +} + +.stat-value { + display: block; + font-size: 1.35rem; + font-weight: 800; +} + +.stat-label { + color: var(--muted); + font-size: 0.88rem; +} + +.card h2 { + margin: 0 0 0.45rem; + font-size: 1.08rem; +} + +.card p { + margin: 0; + color: var(--muted); +} + +.list { + margin: 0; + padding-inline-start: 1.1rem; + display: grid; + gap: 0.35rem; +} + +.site-footer { + border-top: 1px solid var(--line); + background: color-mix(in srgb, var(--surface) 84%, transparent); +} + +.footer-content { + padding: 1rem 0; + display: flex; + justify-content: space-between; + gap: 1rem; + color: var(--muted); + font-size: 0.9rem; +} + +.locale-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 3rem; + padding: 0.2rem 0.6rem; + border: 1px solid var(--line); + border-radius: 999px; +} + +.bottom-nav { + display: flex; + justify-content: center; + padding: 0 0 1rem; + margin-top: 0.5rem; +} + +.bottom-nav-inner { + min-height: 3.2rem; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0; + border: 1px solid color-mix(in srgb, var(--line) 75%, transparent); + border-radius: 999px; + backdrop-filter: blur(20px); + background: color-mix(in srgb, var(--surface) 78%, var(--bg)); + box-shadow: 0 18px 34px rgba(0, 0, 0, 0.25); + padding: 0.34rem; + width: auto; + max-width: min(96vw, 820px); +} + +.bottom-nav-links { + display: flex; + align-items: center; + gap: 0.26rem; + flex-wrap: nowrap; + overflow-x: auto; + scrollbar-width: none; +} + +.bottom-nav-links::-webkit-scrollbar { + display: none; +} + +.bottom-nav-links a { + padding: 0.42rem 0.7rem; + border-radius: 999px; + border: 1px solid transparent; + color: var(--muted); + font-size: 0.82rem; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 0.36rem; + white-space: nowrap; + transition: transform 180ms ease, border-color 180ms ease, color 180ms ease, background-color 180ms ease; +} + +.bottom-nav-links a:hover, +.bottom-nav-links a:focus-visible { + border-color: var(--line); + color: var(--text); + transform: translateY(-1px); + outline: none; +} + +.bottom-nav-links a.active { + color: var(--nav-active-text); + border-color: transparent; + background: var(--nav-active-bg); +} + +.nav-icon { + font-size: 0.72rem; + opacity: 0.9; +} + +.lang-toggle { + margin-inline-start: 0.18rem; + border-color: var(--line) !important; +} + +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 860px) { + .split-grid, + .stat-grid { + grid-template-columns: 1fr; + } + + .bottom-nav-inner { + min-height: 3.05rem; + max-width: min(96vw, 98vw); + } + + .footer-content { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..e24f40c --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "diyaa | Personal Website", + description: "Bilingual personal website built with Next.js", +}; + +const themeScript = ` +(() => { + try { + const storedTheme = localStorage.getItem("theme"); + const activeTheme = storedTheme === "light" || storedTheme === "dark" ? storedTheme : "dark"; + document.documentElement.setAttribute("data-theme", activeTheme); + } catch { + document.documentElement.setAttribute("data-theme", "dark"); + } +})(); +`; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + +