diff --git a/.dockerignore b/.dockerignore index 3bb7ff6..6cece8e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ node_modules .next out dist +tmp npm-debug.log .npmrc .git diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index f96f17a..3280c19 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,8 +6,7 @@ concurrency: on: push: - branches: [master] - tags: ["v*"] + branches: ["**"] pull_request: workflow_dispatch: @@ -20,11 +19,13 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - packages: write steps: + - name: Configure Git default branch + run: git config --global init.defaultBranch master + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Compute image name (lowercase owner/repo) id: img @@ -34,39 +35,42 @@ jobs: IMAGE_NAME="${IMAGE_NAME:-$REPO_NAME}" IMAGE_NAME="$(echo "${IMAGE_NAME}" | tr '[:upper:]' '[:lower:]')" echo "IMAGE=${REGISTRY}/${OWNER}/${IMAGE_NAME}" >> "$GITHUB_ENV" + echo "Image namespace: ${REGISTRY}/${OWNER}/${IMAGE_NAME}" - name: Docker metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.IMAGE }} tags: | type=sha - type=ref,event=tag type=ref,event=branch,enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} - name: Set up Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build image for tests - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile push: false load: true - tags: ${{ env.IMAGE }}:ci + tags: | + ${{ env.IMAGE }}:ci + ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha - cache-to: type=gha,mode=max + cache-to: type=gha,mode=max,ignore-error=true - - name: Run smoke test (curl /) + - name: Run smoke test env: IMAGE_UNDER_TEST: ${{ env.IMAGE }}:ci run: | set -euo pipefail cid=$(docker run -d -p 0:3000 "$IMAGE_UNDER_TEST") - trap "docker rm -f $cid >/dev/null 2>&1" EXIT + trap 'docker rm -f "$cid" >/dev/null 2>&1' EXIT port=$(docker inspect -f '{{ (index (index .NetworkSettings.Ports "3000/tcp") 0).HostPort }}' "$cid") if [ -z "$port" ]; then echo "Failed to resolve mapped port for container $cid" >&2 @@ -75,17 +79,59 @@ jobs: fi for i in {1..20}; do if curl -fsS "http://127.0.0.1:${port}/" > /dev/null; then - exit 0 + break + fi + if [ "$i" -eq 20 ]; then + echo "Service did not respond on / after 20s" >&2 + docker logs "$cid" || true + exit 1 fi sleep 1 done - echo "Service did not respond on / after 20s" >&2 - docker logs "$cid" || true - exit 1 + for path in / /about /services /nabla /presentations /healthz; do + curl -fsS "http://127.0.0.1:${port}${path}" > /dev/null + done + + publish: + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Configure Git default branch + run: git config --global init.defaultBranch master + + - name: Checkout + uses: actions/checkout@v6 + + - name: Compute image name (lowercase owner/repo) + id: img + run: | + OWNER="$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')" + REPO_NAME="${GITHUB_REPOSITORY##*/}" + IMAGE_NAME="${IMAGE_NAME:-$REPO_NAME}" + IMAGE_NAME="$(echo "${IMAGE_NAME}" | tr '[:upper:]' '[:lower:]')" + echo "IMAGE=${REGISTRY}/${OWNER}/${IMAGE_NAME}" >> "$GITHUB_ENV" + echo "Image namespace: ${REGISTRY}/${OWNER}/${IMAGE_NAME}" + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.IMAGE }} + tags: | + type=sha + type=ref,event=branch + type=raw,value=latest + + - name: Set up Buildx + uses: docker/setup-buildx-action@v4 - name: Log in to GHCR - if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/master') || startsWith(github.ref, 'refs/tags/')) - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -93,8 +139,7 @@ jobs: password: ${{ secrets.CR_PAT != '' && secrets.CR_PAT || secrets.GITHUB_TOKEN }} - name: Build and push - if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/master') || startsWith(github.ref, 'refs/tags/')) - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile @@ -102,4 +147,4 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha - cache-to: type=gha,mode=max + cache-to: type=gha,mode=max,ignore-error=true diff --git a/.gitignore b/.gitignore index 69673c1..d8385ec 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,8 @@ yarn-error.log* next-env.d.ts # updating user ssh dir -.ssh \ No newline at end of file +.ssh + +# Transient cleanup +.old_nm_trash* +tmp/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..61e34c2 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +auto-install-peers=false +strict-peer-dependencies=false diff --git a/Caddyfile.site b/Caddyfile.site new file mode 100644 index 0000000..dcbdbc2 --- /dev/null +++ b/Caddyfile.site @@ -0,0 +1,6 @@ +:3000 { + root * /srv + respond /healthz 200 + try_files {path} {path}.html {path}/index.html + file_server +} diff --git a/Dockerfile b/Dockerfile index 33c340f..c809b1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,7 @@ FROM node:22-alpine AS build WORKDIR /app -ENV NODE_ENV=production \ - NEXT_TELEMETRY_DISABLED=1 +ENV NEXT_TELEMETRY_DISABLED=1 RUN apk add --no-cache libc6-compat @@ -20,7 +19,8 @@ WORKDIR /srv RUN addgroup -S caddy && adduser -S caddy -G caddy COPY --from=build --chown=caddy:caddy /app/out ./ +COPY --chown=caddy:caddy Caddyfile.site /etc/caddy/Caddyfile EXPOSE 3000 USER caddy -ENTRYPOINT ["caddy", "file-server", "--root=/srv", "--listen=:3000"] +ENTRYPOINT ["caddy", "run", "--config=/etc/caddy/Caddyfile"] diff --git a/app/about/page.tsx b/app/about/page.tsx index 72df57d..7b7cb75 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,96 +1,19 @@ -import Link from "next/link" -import Image, { StaticImageData } from "next/image"; -import { Paragraph, Chapter } from "../components/TextUtils" -import ContactEmail from "./ContactEmail" - -import vulkanised2026_01 from "@/public/vulkanised_photos/2026/2026_01.jpg"; -import vulkanised2026_02 from "@/public/vulkanised_photos/2026/2026_02.jpg"; -import vulkanised2026_03 from "@/public/vulkanised_photos/2026/2026_03.jpg"; -import vulkanised2026_04 from "@/public/vulkanised_photos/2026/2026_04.jpg"; -import vulkanised2026_06 from "@/public/vulkanised_photos/2026/2026_06.jpg"; -import vulkanised2026_07 from "@/public/vulkanised_photos/2026/2026_07.jpg"; -import vulkanised2026_08 from "@/public/vulkanised_photos/2026/2026_08.jpg"; -import vulkanised2026_09 from "@/public/vulkanised_photos/2026/2026_09.jpg"; -import vulkanised2023_1 from "@/public/vulkanised_photos/2023/2023_1.jpg"; -import vulkanised2023_2 from "@/public/vulkanised_photos/2023/2023_2.jpg"; -import vulkanised2024_1 from "@/public/vulkanised_photos/2024/2024_1.jpg"; -import vulkanised2024_2 from "@/public/vulkanised_photos/2024/2024_2.jpg"; - -const vulkanisedPhotos: { src: StaticImageData; alt: string }[] = [ - { src: vulkanised2026_01, alt: "Vulkanised 2026 photo 1" }, - { src: vulkanised2026_02, alt: "Vulkanised 2026 photo 2" }, - { src: vulkanised2026_04, alt: "Vulkanised 2026 photo 4" }, - { src: vulkanised2026_03, alt: "Vulkanised 2026 photo 3" }, - { src: vulkanised2026_06, alt: "Vulkanised 2026 photo 6" }, - { src: vulkanised2026_08, alt: "Vulkanised 2026 photo 8" }, - { src: vulkanised2026_09, alt: "Vulkanised 2026 photo 9" }, - { src: vulkanised2026_07, alt: "Vulkanised 2026 photo 7" }, - { src: vulkanised2023_1, alt: "Vulkanised 2023 photo 1" }, - { src: vulkanised2023_2, alt: "Vulkanised 2023 photo 2" }, - { src: vulkanised2024_1, alt: "Vulkanised 2024 photo 1" }, - { src: vulkanised2024_2, alt: "Vulkanised 2024 photo 2" } -]; - -function VulkanisedPhoto({ src, alt, priority = false }: { src: StaticImageData | string, alt: string, priority?: boolean }) { - return ( -
- - {alt} -
- ) -} +import { Paragraph, Chapter } from "../components/TextUtils"; +import { aboutParagraphs } from "../data/aboutContent"; export default function Page() { return ( -
- +
+ - DevSH Graphics Programming Sp. z O.O is a company focused on Graphics, GPU and High Performance Computing. Our consultants develop and maintain Renderers, Simulations and Compilers for our Clients, integrated into or working alongside their teams. We are not a Software House, we work very closely and synergize with our Clients' engineers. - We also conduct our own R&D developing our own Open Source Middleware and Libraries, the most prominent being Nabla, as well as contributing to existing ones. -
-
- The primary mission for all of our self-funded developments is to advance Open Source ecosystems with innovative tooling with a particular focus on Khronos Standards. We maintain a single source HLSL202x/C++20 Standard Template Header Only Library and our Utility and Rapid Prototyping Framework Nabla designed {/*this prevents visual bug, "designed must stay here"*/} - to give a CUDA-like programming experience within the Vulkan ecosystem. -
-
- We have honed the culture of remote work, since the company's inception, and way before the 2019 paradigm shift. Subject to availability and specific expertise required, our consultants' regular working hours overlap the normal working hours from San Francisco to Sydney. -
-
- Our alumni have since worked at Intel, Huawei, ARM and Apple as driver and devtech developers and on AAA games. -
- -
- {vulkanisedPhotos.map((photo, index) => ( - + {aboutParagraphs.map((paragraph, index) => ( + + {paragraph} + {index < aboutParagraphs.length - 1 && <>

} +
))} -
-
- - - If you're interested in our offer, you can reach us at this e-mail address:
- ) + ); } diff --git a/app/components/AboutSection.tsx b/app/components/AboutSection.tsx new file mode 100644 index 0000000..3aeb795 --- /dev/null +++ b/app/components/AboutSection.tsx @@ -0,0 +1,26 @@ +import CTAButton from "./CTAButton"; +import { aboutParagraphs } from "../data/aboutContent"; + +export default function AboutSection() { + return ( +
+
+
+

Who We Are

+
+ +
+ {aboutParagraphs.map((paragraph, index) => ( +

+ {paragraph} +

+ ))} +
+ +
+ +
+
+
+ ); +} diff --git a/app/components/CTAButton.tsx b/app/components/CTAButton.tsx new file mode 100644 index 0000000..3b0b68c --- /dev/null +++ b/app/components/CTAButton.tsx @@ -0,0 +1,41 @@ +type Size = "md" | "lg"; + +export default function CTAButton({ + href = "#", + label = "Talk to our experts", + size = "lg", +}: { + href?: string; + label?: string; + size?: Size; +}) { + const sizing = + size === "lg" + ? "w-full max-w-64 sm:w-auto sm:min-w-56 px-4 py-2.5 text-sm sm:px-5 sm:text-base" + : "px-4 py-2.5 text-sm sm:px-5 sm:text-base"; + + return ( + + {label} + + + + + ); +} diff --git a/app/components/EcosystemSection.tsx b/app/components/EcosystemSection.tsx new file mode 100644 index 0000000..c5e4841 --- /dev/null +++ b/app/components/EcosystemSection.tsx @@ -0,0 +1,108 @@ +import Image from "next/image"; + +type Photo = { caption: string; image: string | null }; + +const photos: Photo[] = [ + { caption: "Presenting at Shading Language Symposium 2026", image: "/vulkanised_photos/2026/2026_02.jpg" }, + { caption: "Our booth at Vulkanised 2026", image: "/vulkanised_photos/2026/2026_10.jpg" }, + { caption: "Presenting at Vulkanised 2023", image: "/vulkanised_photos/2023/2023_1.jpg" }, + { caption: "Presenting at Vulkanised 2026", image: "/vulkanised_photos/2026/2026_09.jpg" }, +]; +function PhotoCard({ photo }: { photo: Photo }) { + return ( +
+
+ {photo.image ? ( + {photo.caption} + ) : ( +
+ +
+ )} +
+ +
+

+ {photo.caption} +

+
+ ); +} + +export default function EcosystemSection() { + return ( +
+
+ ); +} diff --git a/app/components/ExpertiseGrid.tsx b/app/components/ExpertiseGrid.tsx new file mode 100644 index 0000000..06f8d95 --- /dev/null +++ b/app/components/ExpertiseGrid.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Image from "next/image"; + +const items: { title: string; images: string[]; href: string }[] = [ + { + title: "Real-Time Graphics & Engine Optimization", + images: ["/clients/baw/baw4.jpg"], + href: "#projects" + }, + { + title: "Path Tracing and Physically-Based Rendering", + images: [ + "/nabla/rt_screenshot_both.jpg", + "/clients/ditt/ditt4.png", + "/clients/ditt/ditt5.jpg", + "/clients/ditt/ditt2.jpg" + ], + href: "#project-ditt" + }, + { + title: "CAD & Scientific Visualization", + images: ["/clients/apps_in_cadd/scene1_cropped.png"], + href: "#project-appscadd" + }, + { + title: "VR & Mobile GPU Development", + images: ["/clients/wild/wild3.jpg"], + href: "#project-wild" + }, + { + title: "Computational Geometry", + images: ["/clients/apps_in_cadd/offset_curve.gif"], + href: "#project-appscadd" + }, + { + title: "High-Performance Compute & Optimization", + images: ["/nabla/nsc.png"], + href: "#projects" + }, + { + title: "Photogrammetry and Differentiable Rendering", + images: ["/clients/baw/volume_reconstruct.png"], + href: "#project-buildaworld" + }, +]; + +function PlaceholderIcon() { + return ( + + ); +} + +function Card({ title, images, href }: { title: string; images: string[]; href: string }) { + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + if (images.length <= 1) return; + + const interval = setInterval(() => { + setCurrentIndex((prevIndex) => (prevIndex + 1) % images.length); + }, 6000); + + return () => clearInterval(interval); + }, [images.length]); + + return ( + + {images.length > 0 ? ( + // Keep slides stacked so opacity transitions do not flash to black. + images.map((src, index) => ( + {`${title} + )) + ) : ( + <> +
+
+ +
+ + )} + +
+ +
+

+ {title} +

+
+
+ ); +} + +export default function ExpertiseGrid() { + return ( +
+
+
+

Our Expertise

+
+ {/* Two-column mobile grid, then a wrapped centered row on wider screens. */} +
+ {items.map((it) => ( + + ))} +
+
+
+ ); +} diff --git a/app/components/HeroHeader.tsx b/app/components/HeroHeader.tsx new file mode 100644 index 0000000..8edd176 --- /dev/null +++ b/app/components/HeroHeader.tsx @@ -0,0 +1,33 @@ +import CTAButton from "./CTAButton"; + +export default function HeroHeader() { + return ( +
+
+ ); +} diff --git a/app/components/NablaSection.tsx b/app/components/NablaSection.tsx new file mode 100644 index 0000000..4151fda --- /dev/null +++ b/app/components/NablaSection.tsx @@ -0,0 +1,287 @@ +"use client"; + +import Image from "next/image"; +import type { ReactNode } from "react"; +import { useCallback, useEffect, useState } from "react"; + +const NABLA = { + repoUrl: "https://github.com/Devsh-Graphics-Programming/Nabla", + stats: { + stars: "685", + forks: "73", + openPRs: "40", + commits: "15,969", + }, + highlights: [ + "Single-source HLSL202x/C++20 Standard Template Header-Only Library", + "SPIR-V and Vulkan as First-Class Citizens", + "Utilities for Rapid Prototyping for modern GPU Workloads (raytracing, compute, raster, etc.)", + "Unit-Tested BxDFs for Physically Based Rendering", + "Intelligent GPU Object Lifecycle Tracking and Resource Management", + ], + slides: [ + { src: "/nabla/rt_screenshot_both.jpg", caption: "Raytracing" }, + { src: "/nabla/fluid_sim.gif", caption: "Fluid Simulation" }, + { src: "/nabla/stipples.gif", caption: "GPU-Accelerated Vectorized Linework" }, + { src: "/nabla/nsc.png", caption: "Nabla Shader Compiler & Godbolt docker integration" }, + { src: "/nabla/fft_bloom_heart.gif", caption: "Fast Fourier Transform Bloom" }, + { src: "/nabla/imguiintegration.jpg", caption: "ImGui Integration" }, + { src: "/nabla/2d_csg.gif", caption: "2D Constructive Solid Geometry" }, + { src: "/nabla/Iridescence.png", caption: "Iridescent Materials" }, + ], +}; + +const AUTO_MS = 4500; + +function GitHubIcon({ className = "h-5 w-5" }: { className?: string }) { + return ( + + ); +} + +function StarIcon() { + return ( + + ); +} + +function ForkIcon() { + return ( + + ); +} + +function DotIcon() { + return ( + + ); +} + +function StatBadge({ icon, value, label }: { icon: ReactNode; value: string; label: string }) { + return ( +
+
+ {icon} + {value} +
+

{label}

+
+ ); +} + +function Slideshow() { + const slides = NABLA.slides; + const [active, setActive] = useState(0); + + // Touch state for swiping + const [touchStart, setTouchStart] = useState(null); + const [touchEnd, setTouchEnd] = useState(null); + + // Minimum distance (in pixels) to trigger a swipe + const minSwipeDistance = 50; + + const goTo = useCallback((idx: number) => { + setActive(idx); + }, []); + + const nextSlide = useCallback(() => { + setActive((current) => (current + 1) % slides.length); + }, [slides.length]); + + const prevSlide = useCallback(() => { + setActive((current) => (current - 1 + slides.length) % slides.length); + }, [slides.length]); + + // Touch Event Handlers + const onTouchStart = (e: React.TouchEvent) => { + setTouchEnd(null); // Reset touch end to prevent false positives from previous swipes + setTouchStart(e.targetTouches[0].clientX); + }; + + const onTouchMove = (e: React.TouchEvent) => { + setTouchEnd(e.targetTouches[0].clientX); + }; + + const onTouchEnd = () => { + if (!touchStart || !touchEnd) return; + + const distance = touchStart - touchEnd; + const isLeftSwipe = distance > minSwipeDistance; + const isRightSwipe = distance < -minSwipeDistance; + + if (isLeftSwipe) { + nextSlide(); + } else if (isRightSwipe) { + prevSlide(); + } + }; + + // Auto-play timer + useEffect(() => { + const interval = setInterval(nextSlide, AUTO_MS); + // Adding `active` to the dependency array ensures the timer resets + // whenever the user manually changes the slide (via swipe or dots). + return () => clearInterval(interval); + }, [nextSlide, active]); + + return ( +
+
+ {/* Render all slides stacked, fading opacity/blur via CSS */} + {slides.map((slide, index) => { + const isActive = index === active; + + return ( +
+ {slide.caption} +
+

{slide.caption}

+
+
+ ); + })} +
+ +
+ {slides.map((slideItem, index) => ( +
+
+ ); +} + +function NablaGlyph({ className = "h-12 w-12 sm:h-14 sm:w-14" }: { className?: string }) { + return ( + + + Nabla + + ); +} + +function NablaBackdrop() { + return ( + <> +