Compare commits
8 Commits
633be4b4e4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 71a58fe0f2 | |||
| e6d07d05cd | |||
| 762b03c291 | |||
| 98059d50bc | |||
| d84e6e20bd | |||
| fa50cd39b4 | |||
| fef4853069 | |||
| 5ad6ce7894 |
+28
@@ -0,0 +1,28 @@
|
||||
# ---------- BUILD STAGE ----------
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# ---------- RUN STAGE ----------
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# nur notwendige files kopieren (viel kleiner!)
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,9 @@
|
||||
export default function About() {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<p>Nico Haider</p>
|
||||
<p>3843 Dobersberg</p>
|
||||
<p>Portfolio</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,7 @@ export default function RootLayout({
|
||||
<div className="relative z-10 flex-1">
|
||||
{children}
|
||||
</div>
|
||||
<span className="p-5 text-center font-light text-sm text-muted-foreground">© 2026 Nico Haider</span>
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
|
||||
+22
-5
@@ -1,13 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { Fragment, useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ThemeSwitch } from "../../components/custom/theme-switch"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { LocaleSwitch } from "../../components/custom/locale-switch"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Menu, X } from "lucide-react"
|
||||
import { Link, usePathname } from "@/i18n/navigation"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
export default function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const pathname = usePathname()
|
||||
const t = useTranslations('pages');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -47,6 +48,12 @@ export default function Navbar() {
|
||||
{ href: "/projects", label: t("projects") },
|
||||
];
|
||||
|
||||
const isActiveHref = (href: string) => {
|
||||
if (!pathname) return false
|
||||
if (href === "/") return pathname === "/"
|
||||
return pathname === href || pathname.startsWith(`${href}/`)
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 left-0 w-full z-50 flex justify-center p-4 md:p-10">
|
||||
|
||||
@@ -63,12 +70,19 @@ export default function Navbar() {
|
||||
: "border-foreground/0"
|
||||
)}
|
||||
>
|
||||
<h1 className={cn("text-4xl font-medium")}>bH</h1>
|
||||
<Link href="/"><h1 className={cn("text-4xl font-medium")}>bH</h1></Link>
|
||||
|
||||
<ul className="hidden md:flex items-center gap-8 text-sm text-foreground/60">
|
||||
{navLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link href={link.href} className="hover:text-foreground transition">
|
||||
<Link
|
||||
href={link.href}
|
||||
aria-current={isActiveHref(link.href) ? "page" : undefined}
|
||||
className={cn(
|
||||
"transition hover:text-foreground",
|
||||
isActiveHref(link.href) && "text-foreground"
|
||||
)}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
@@ -99,8 +113,11 @@ export default function Navbar() {
|
||||
>
|
||||
{navLinks.map((link, index) => (
|
||||
<Fragment key={link.href}>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={link.href}>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className={cn(isActiveHref(link.href) && "font-medium text-foreground")}
|
||||
>
|
||||
<Link href={link.href} aria-current={isActiveHref(link.href) ? "page" : undefined}>
|
||||
{link.label}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
+69
-11
@@ -1,8 +1,12 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TypographyH1, TypographyLead } from "../../components/ui/typography";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { MapPin } from "lucide-react";
|
||||
import { Mail, MapPin, Phone } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { InstagramIcon } from "../../components/icons/instagram";
|
||||
import { LinkedInIcon } from "../../components/icons/linkedin";
|
||||
import { FacebookIcon } from "../../components/icons/facebook";
|
||||
|
||||
export default function Home() {
|
||||
const t = useTranslations();
|
||||
@@ -17,16 +21,70 @@ export default function Home() {
|
||||
{t('landingPage.location')}
|
||||
</Badge>
|
||||
</div>
|
||||
<Image
|
||||
src="/me.png"
|
||||
alt="Picture of me"
|
||||
width={380}
|
||||
height={380}
|
||||
style={{
|
||||
filter: "drop-shadow(10px 0 25px rgb(from color-mix(in oklch, var(--foreground) 9%, transparent) r g b / .05))",
|
||||
}}
|
||||
className="mask-[linear-gradient(to_bottom,#000_80%,#0000)]"
|
||||
/>
|
||||
<div>
|
||||
<Image
|
||||
src="/me.png"
|
||||
alt="Picture of me"
|
||||
width={380}
|
||||
height={380}
|
||||
style={{
|
||||
filter: "drop-shadow(10px 0 25px rgb(from color-mix(in oklch, var(--foreground) 9%, transparent) r g b / .05))",
|
||||
}}
|
||||
className="mask-[linear-gradient(to_bottom,#000_80%,#0000)]"
|
||||
loading="eager"
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-center gap-3 flex-wrap">
|
||||
<Button asChild variant="outline">
|
||||
<a
|
||||
href="mailto:nico@byhaider.dev"
|
||||
aria-label={t("landingPage.contact.email")}
|
||||
>
|
||||
<Mail />
|
||||
nico@byhaider.dev
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<a href="tel:+436702060140" aria-label={t("landingPage.contact.phone")}>
|
||||
<Phone />
|
||||
+43 670 2060140
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Button asChild variant="ghost" size="icon">
|
||||
<a
|
||||
href="https://www.linkedin.com/in/nico-haider-164444316"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="sr-only">{t("landingPage.social.linkedin")}</span>
|
||||
<LinkedInIcon className="size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" size="icon">
|
||||
<a
|
||||
href="https://www.instagram.com/nico.hdr8/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="sr-only">{t("landingPage.social.instagram")}</span>
|
||||
<InstagramIcon className="size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" size="icon">
|
||||
<a
|
||||
href="https://www.facebook.com/nico.haider.33/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="sr-only">{t("landingPage.social.facebook")}</span>
|
||||
<FacebookIcon className="size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export default function Projects() {
|
||||
return (
|
||||
<p className="text-center">in work ...</p>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
type FacebookIconProps = React.ComponentProps<"svg"> & {
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function FacebookIcon({ title, ...props }: FacebookIconProps) {
|
||||
const ariaHidden = title ? undefined : true
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 640"
|
||||
fill="currentColor"
|
||||
aria-hidden={ariaHidden}
|
||||
role={title ? "img" : undefined}
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
{title ? <title>{title}</title> : null}
|
||||
<path d="M576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 440 146.7 540.8 258.2 568.5L258.2 398.2L205.4 398.2L205.4 320L258.2 320L258.2 286.3C258.2 199.2 297.6 158.8 383.2 158.8C399.4 158.8 427.4 162 438.9 165.2L438.9 236C432.9 235.4 422.4 235 409.3 235C367.3 235 351.1 250.9 351.1 292.2L351.1 320L434.7 320L420.3 398.2L351 398.2L351 574.1C477.8 558.8 576 450.9 576 320z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
type InstagramIconProps = React.ComponentProps<"svg"> & {
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function InstagramIcon({ title, ...props }: InstagramIconProps) {
|
||||
const ariaHidden = title ? undefined : true
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 640"
|
||||
fill="currentColor"
|
||||
aria-hidden={ariaHidden}
|
||||
role={title ? "img" : undefined}
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
{title ? <title>{title}</title> : null}
|
||||
<path d="M320.3 205C256.8 204.8 205.2 256.2 205 319.7C204.8 383.2 256.2 434.8 319.7 435C383.2 435.2 434.8 383.8 435 320.3C435.2 256.8 383.8 205.2 320.3 205zM319.7 245.4C360.9 245.2 394.4 278.5 394.6 319.7C394.8 360.9 361.5 394.4 320.3 394.6C279.1 394.8 245.6 361.5 245.4 320.3C245.2 279.1 278.5 245.6 319.7 245.4zM413.1 200.3C413.1 185.5 425.1 173.5 439.9 173.5C454.7 173.5 466.7 185.5 466.7 200.3C466.7 215.1 454.7 227.1 439.9 227.1C425.1 227.1 413.1 215.1 413.1 200.3zM542.8 227.5C541.1 191.6 532.9 159.8 506.6 133.6C480.4 107.4 448.6 99.2 412.7 97.4C375.7 95.3 264.8 95.3 227.8 97.4C192 99.1 160.2 107.3 133.9 133.5C107.6 159.7 99.5 191.5 97.7 227.4C95.6 264.4 95.6 375.3 97.7 412.3C99.4 448.2 107.6 480 133.9 506.2C160.2 532.4 191.9 540.6 227.8 542.4C264.8 544.5 375.7 544.5 412.7 542.4C448.6 540.7 480.4 532.5 506.6 506.2C532.8 480 541 448.2 542.8 412.3C544.9 375.3 544.9 264.5 542.8 227.5zM495 452C487.2 471.6 472.1 486.7 452.4 494.6C422.9 506.3 352.9 503.6 320.3 503.6C287.7 503.6 217.6 506.2 188.2 494.6C168.6 486.8 153.5 471.7 145.6 452C133.9 422.5 136.6 352.5 136.6 319.9C136.6 287.3 134 217.2 145.6 187.8C153.4 168.2 168.5 153.1 188.2 145.2C217.7 133.5 287.7 136.2 320.3 136.2C352.9 136.2 423 133.6 452.4 145.2C472 153 487.1 168.1 495 187.8C506.7 217.3 504 287.3 504 319.9C504 352.5 506.7 422.6 495 452z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
type LinkedInIconProps = React.ComponentProps<"svg"> & {
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function LinkedInIcon({ title, ...props }: LinkedInIconProps) {
|
||||
const ariaHidden = title ? undefined : true
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 640"
|
||||
fill="currentColor"
|
||||
aria-hidden={ariaHidden}
|
||||
role={title ? "img" : undefined}
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
{title ? <title>{title}</title> : null}
|
||||
<path d="M512 96L127.9 96C110.3 96 96 110.5 96 128.3L96 511.7C96 529.5 110.3 544 127.9 544L512 544C529.6 544 544 529.5 544 511.7L544 128.3C544 110.5 529.6 96 512 96zM231.4 480L165 480L165 266.2L231.5 266.2L231.5 480L231.4 480zM198.2 160C219.5 160 236.7 177.2 236.7 198.5C236.7 219.8 219.5 237 198.2 237C176.9 237 159.7 219.8 159.7 198.5C159.7 177.2 176.9 160 198.2 160zM480.3 480L413.9 480L413.9 376C413.9 351.2 413.4 319.3 379.4 319.3C344.8 319.3 339.5 346.3 339.5 374.2L339.5 480L273.1 480L273.1 266.2L336.8 266.2L336.8 295.4L337.7 295.4C346.6 278.6 368.3 260.9 400.6 260.9C467.8 260.9 480.3 305.2 480.3 362.8L480.3 480z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
services:
|
||||
homepage:
|
||||
build:
|
||||
context: .
|
||||
image: byhaider-homepage:latest
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.byhaider-homepage.rule=Host(`byhaider.dev`)"
|
||||
- "traefik.http.routers.byhaider-homepage.entrypoints=websecure"
|
||||
- "traefik.http.routers.byhaider-homepage.tls=true"
|
||||
- "traefik.http.routers.byhaider-homepage.tls.certResolver=cloudflare"
|
||||
- "traefik.http.services.byhaider-homepage.loadbalancer.server.port=3000"
|
||||
networks:
|
||||
- traefik-network
|
||||
|
||||
networks:
|
||||
traefik-network:
|
||||
external: true
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 Starte Deployment für byHaider-Homepage..."
|
||||
|
||||
cd ~/docker-setup/byhaider-homepage
|
||||
|
||||
echo "📥 Pull latest changes for byhaider-homepage..."
|
||||
git pull origin main
|
||||
|
||||
echo "🛑 Stoppe alte Container..."
|
||||
docker compose down
|
||||
|
||||
echo "📦 Baue Images neu..."
|
||||
docker compose build
|
||||
|
||||
echo "⬆️ Starte Container..."
|
||||
docker compose up -d
|
||||
|
||||
echo "🧹 Bereinige alte Images..."
|
||||
docker image prune -f
|
||||
|
||||
echo "✅ Deployment abgeschlossen!"
|
||||
docker compose ps
|
||||
|
||||
+10
-1
@@ -16,6 +16,15 @@
|
||||
"landingPage": {
|
||||
"title": "Hi, ich bin Nico Haider.",
|
||||
"subtitle": "Softwareentwickler für Webseiten, Web-Apps, Mobile-Apps, Desktop-Apps und vieles mehr.",
|
||||
"location": "Dobersberg, Niederösterreich"
|
||||
"location": "Dobersberg, Niederösterreich",
|
||||
"contact": {
|
||||
"email": "E-Mail senden",
|
||||
"phone": "Telefonnummer anrufen"
|
||||
},
|
||||
"social": {
|
||||
"linkedin": "LinkedIn-Profil öffnen",
|
||||
"instagram": "Instagram-Profil öffnen",
|
||||
"facebook": "Facebook-Profil öffnen"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,19 @@
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"landingPage": {
|
||||
"title": "Hi, I'm Nico Haider.",
|
||||
"subtitle": "Software developer for websites, web apps, mobile apps, desktop apps, and more.",
|
||||
"location": "Dobersberg, Lower Austria",
|
||||
"contact": {
|
||||
"email": "Send email",
|
||||
"phone": "Call phone number"
|
||||
},
|
||||
"social": {
|
||||
"linkedin": "Open LinkedIn profile",
|
||||
"instagram": "Open Instagram profile",
|
||||
"facebook": "Open Facebook profile"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import createNextIntlPlugin from "next-intl/plugin";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
Reference in New Issue
Block a user