-
-}
\ No newline at end of file
diff --git a/src/components/animations/NodesAnimation.tsx b/src/components/animations/NodesAnimation.tsx
new file mode 100644
index 0000000..6a5f860
--- /dev/null
+++ b/src/components/animations/NodesAnimation.tsx
@@ -0,0 +1,240 @@
+"use client"
+
+import { Card, Flex, Text } from "@code0-tech/pictor"
+import { IconNote } from "@tabler/icons-react"
+import { animate, m as motion, useInView, useReducedMotion } from "motion/react"
+import { useEffect, useRef, useState } from "react"
+import { LiteralBadge } from "../badges/LiteralBadge"
+import { ReferenceBadge } from "../badges/ReferenceBadge"
+import { NodeBadge } from "../badges/NodeBadge"
+
+type NodeSegmentType = "text" | "literal" | "reference" | "node"
+type NodeAccent = "brand" | "yellow" | "aqua" | "blue" | "pink"
+
+interface NodeSegment {
+ type: NodeSegmentType
+ value: string
+}
+
+interface NodeItem {
+ color: NodeAccent
+ segments: NodeSegment[]
+ outline: boolean
+}
+
+function NodeRow({
+ nodes,
+ direction,
+ active,
+}: {
+ nodes: NodeItem[]
+ direction: "left" | "right"
+ active: boolean
+}) {
+ const marqueeRef = useRef
(null)
+ const listRef = useRef(null)
+ const [loopDistance, setLoopDistance] = useState(0)
+ const groupGap = 16
+ const iconColorMap: Record = {
+ brand: "var(--text-brand)",
+ yellow: "var(--text-yellow)",
+ aqua: "var(--text-aqua)",
+ blue: "var(--text-blue)",
+ pink: "var(--text-pink)",
+ }
+
+ const displayMessage = (segments: NodeSegment[]) => {
+ return segments.map((segment, index) => {
+ switch (segment.type) {
+ case "literal":
+ return
+ case "reference":
+ return
+ case "node":
+ return
+ case "text":
+ return
+ {segment.value}
+
+ }
+ })
+ }
+
+ useEffect(() => {
+ const listElement = listRef.current
+ if (!listElement) return
+
+ const updateLoopDistance = () => {
+ setLoopDistance(listElement.getBoundingClientRect().width + groupGap)
+ }
+
+ updateLoopDistance()
+
+ const resizeObserver = new ResizeObserver(updateLoopDistance)
+ resizeObserver.observe(listElement)
+
+ return () => resizeObserver.disconnect()
+ }, [nodes.length])
+
+ useEffect(() => {
+ const marqueeElement = marqueeRef.current
+ if (!marqueeElement || !loopDistance || !active) return
+
+ const controls = animate(
+ marqueeElement,
+ { x: direction === "left" ? [0, -loopDistance] : [-loopDistance, 0] },
+ {
+ duration: 45,
+ ease: "linear",
+ repeat: Infinity,
+ repeatType: "loop",
+ },
+ )
+
+ return () => controls.stop()
+ }, [active, direction, loopDistance])
+
+ return (
+
+
+ {nodes.map((node, index) => (
+
+
+
+
+ {displayMessage(node.segments)}
+
+
+
+ ))}
+
+
+ {nodes.map((node, index) => (
+
+
+
+
+ {displayMessage(node.segments)}
+
+
+
+ ))}
+
+
+ )
+}
+
+export function NodesAnimation() {
+ const containerRef = useRef(null)
+ const isInView = useInView(containerRef, { amount: 0.2 })
+ const prefersReducedMotion = useReducedMotion()
+ const nodes: NodeItem[] = [
+ {
+ color: "pink",
+ outline: true,
+ segments: [
+ { type: "text", value: "Convert" },
+ { type: "reference", value: "value" },
+ { type: "text", value: "to boolean" },
+ ],
+ },
+ {
+ color: "brand",
+ outline: true,
+ segments: [
+ { type: "text", value: "Use fallback" },
+ { type: "literal", value: "false" },
+ { type: "text", value: "when empty" },
+ ],
+ },
+ {
+ color: "yellow",
+ outline: true,
+ segments: [
+ { type: "text", value: "Run node" },
+ { type: "node", value: "formatDate" },
+ { type: "text", value: "with current input" },
+ ],
+ },
+ {
+ color: "blue",
+ outline: true,
+ segments: [
+ { type: "text", value: "Only continue if status equals approved" },
+ ],
+ },
+ {
+ color: "aqua",
+ outline: true,
+ segments: [
+ { type: "text", value: "Map" },
+ { type: "reference", value: "user.email" },
+ { type: "text", value: "to contact field" },
+ ],
+ },
+ {
+ color: "blue",
+ outline: true,
+ segments: [
+ { type: "text", value: "Set timeout to" },
+ { type: "literal", value: "30" },
+ { type: "text", value: "seconds" },
+ ],
+ },
+ {
+ color: "brand",
+ outline: true,
+ segments: [
+ { type: "text", value: "Trigger" },
+ { type: "node", value: "sendMail" },
+ { type: "text", value: "after validation" },
+ ],
+ },
+ {
+ color: "pink",
+ outline: true,
+ segments: [
+ { type: "text", value: "This node only contains plain text" },
+ ],
+ },
+ {
+ color: "yellow",
+ outline: true,
+ segments: [
+ { type: "text", value: "Compare" },
+ { type: "reference", value: "invoice.total" },
+ { type: "text", value: "with" },
+ { type: "literal", value: "1000" },
+ ],
+ },
+ ]
+
+ const splitIndex = Math.ceil(nodes.length / 2)
+ const topRowNodes = nodes.slice(0, splitIndex)
+ const bottomRowNodes = nodes.slice(splitIndex)
+
+ return (
+
+ )
+}
diff --git a/src/components/animations/OrbitingCircles.tsx b/src/components/animations/OrbitingCircles.tsx
new file mode 100644
index 0000000..bd7177f
--- /dev/null
+++ b/src/components/animations/OrbitingCircles.tsx
@@ -0,0 +1,73 @@
+import React from "react"
+
+import { cn } from "@/lib/utils"
+
+interface OrbitingCirclesProps extends React.HTMLAttributes {
+ className?: string
+ children?: React.ReactNode
+ reverse?: boolean
+ duration?: number
+ delay?: number
+ radius?: number
+ path?: boolean
+ iconSize?: number
+ speed?: number
+}
+
+export function OrbitingCircles({
+ className,
+ children,
+ reverse,
+ duration = 20,
+ radius = 160,
+ path = true,
+ iconSize = 30,
+ speed = 1,
+ ...props
+}: OrbitingCirclesProps) {
+ const calculatedDuration = duration / speed
+ return (
+ <>
+ {path && (
+
+ )}
+ {React.Children.map(children, (child, index) => {
+ const angle = (360 / React.Children.count(children)) * index
+ return (
+
+ )
+ })}
+ >
+ )
+}
diff --git a/src/components/animations/RoleSystemAnimation.tsx b/src/components/animations/RoleSystemAnimation.tsx
new file mode 100644
index 0000000..a0ac85c
--- /dev/null
+++ b/src/components/animations/RoleSystemAnimation.tsx
@@ -0,0 +1,101 @@
+"use client"
+
+import { Badge, Card, Text } from "@code0-tech/pictor"
+import { animate, m as motion, useInView, useReducedMotion } from "motion/react"
+import { useEffect, useRef, useState } from "react"
+
+interface RoleItem {
+ name: string
+ description: string
+ badges: string[]
+ updatedAt: string
+}
+
+interface RoleSystemAnimationProps {
+ roles: RoleItem[]
+}
+
+export function RoleSystemAnimation({ roles }: RoleSystemAnimationProps) {
+ const containerRef = useRef(null)
+ const marqueeRef = useRef(null)
+ const listRef = useRef(null)
+ const isInView = useInView(containerRef, { amount: 0.2 })
+ const prefersReducedMotion = useReducedMotion()
+ const [loopDistance, setLoopDistance] = useState(0)
+ const groupGap = 16
+
+ useEffect(() => {
+ const listElement = listRef.current
+ if (!listElement) return
+
+ const updateLoopDistance = () => {
+ setLoopDistance(listElement.getBoundingClientRect().height + groupGap)
+ }
+
+ updateLoopDistance()
+
+ const resizeObserver = new ResizeObserver(updateLoopDistance)
+ resizeObserver.observe(listElement)
+
+ return () => resizeObserver.disconnect()
+ }, [roles.length])
+
+ useEffect(() => {
+ const marqueeElement = marqueeRef.current
+ if (!marqueeElement || !loopDistance || !isInView || prefersReducedMotion) return
+
+ const controls = animate(
+ marqueeElement,
+ { y: [0, -loopDistance] },
+ {
+ duration: 20,
+ ease: "linear",
+ repeat: Infinity,
+ repeatType: "loop",
+ },
+ )
+
+ return () => controls.stop()
+ }, [isInView, loopDistance, prefersReducedMotion])
+
+ if (!roles.length) return null
+
+ const renderRoleCard = (role: RoleItem, index: number) => (
+
+ {role.name}
+
+ {role.description}
+ {role.badges.map((badge) => (
+
+ {badge}
+
+ ))}
+
+
+ )
+
+ return (
+
+
+
+ {roles.map(renderRoleCard)}
+
+
+ {roles.map((role, index) => renderRoleCard(role, index + roles.length))}
+
+
+
+ )
+}
diff --git a/src/components/badges/HeroBadge.tsx b/src/components/badges/HeroBadge.tsx
new file mode 100644
index 0000000..b9b87b4
--- /dev/null
+++ b/src/components/badges/HeroBadge.tsx
@@ -0,0 +1,15 @@
+"use client"
+
+import { Badge } from "@code0-tech/pictor"
+import { IconArrowRight } from "@tabler/icons-react"
+
+export function HeroBadge({ badge }: { badge?: string | null }) {
+ if (!badge) return null
+
+ return (
+
+ {badge}
+
+
+ )
+}
diff --git a/src/components/badges/LiteralBadge.tsx b/src/components/badges/LiteralBadge.tsx
new file mode 100644
index 0000000..2684ebe
--- /dev/null
+++ b/src/components/badges/LiteralBadge.tsx
@@ -0,0 +1,17 @@
+"use client"
+
+import {Badge, Text} from "@code0-tech/pictor"
+
+export function LiteralBadge({ value }: { value: string }) {
+ return (
+
+
+ {value}
+
+
+ )
+}
diff --git a/src/components/badges/NodeBadge.tsx b/src/components/badges/NodeBadge.tsx
new file mode 100644
index 0000000..c71c0b6
--- /dev/null
+++ b/src/components/badges/NodeBadge.tsx
@@ -0,0 +1,20 @@
+"use client"
+
+import { Badge, Text } from "@code0-tech/pictor"
+import { IconNote } from "@tabler/icons-react"
+
+export function NodeBadge({ value }: { value: string }) {
+ return (
+
+
+
+ {value}
+
+
+ )
+}
diff --git a/src/components/badges/ReferenceBadge.tsx b/src/components/badges/ReferenceBadge.tsx
new file mode 100644
index 0000000..655aba7
--- /dev/null
+++ b/src/components/badges/ReferenceBadge.tsx
@@ -0,0 +1,21 @@
+"use client"
+
+import { Badge, Text } from "@code0-tech/pictor";
+import { IconVariable } from "@tabler/icons-react";
+
+export function ReferenceBadge({ value }: { value: string }) {
+ return (
+
+
+
+ {value}
+
+
+ )
+}
diff --git a/src/components/blog/BlogPost.tsx b/src/components/blog/BlogPost.tsx
new file mode 100644
index 0000000..f02abd8
--- /dev/null
+++ b/src/components/blog/BlogPost.tsx
@@ -0,0 +1,140 @@
+import { getBlogPostBySlug } from "@/lib/cms"
+import type { AppLocale } from "@/lib/i18n"
+import type { Blog, Media, User } from "@/payload-types"
+import { IconArrowLeft } from "@tabler/icons-react"
+import { convertLexicalToHTML } from "@payloadcms/richtext-lexical/html"
+import Image from "next/image"
+import Link from "next/link"
+import { notFound } from "next/navigation"
+import { MarkdownContent } from "../MarkdownContent"
+import { TableOfContents, type TocHeading } from "./TableOfContents"
+import { LinkButton } from "../ui/LinkButton"
+
+interface BlogPostProps {
+ slug: string
+ locale: AppLocale
+}
+
+interface LexicalNode {
+ type?: string
+ tag?: string
+ text?: string
+ children?: LexicalNode[]
+}
+
+const slugify = (value: string) =>
+ value
+ .toLowerCase()
+ .trim()
+ .replace(/[^\w\s-]/g, "")
+ .replace(/\s+/g, "-")
+ .replace(/-+/g, "-")
+
+const extractText = (node?: LexicalNode): string => {
+ if (!node) return ""
+ if (typeof node.text === "string") return node.text
+ if (!Array.isArray(node.children)) return ""
+ return node.children.map((child) => extractText(child)).join("")
+}
+
+const getTocHeadings = (content: Blog["content"]): TocHeading[] => {
+ const rootChildren = (content as { root?: { children?: LexicalNode[] } })?.root?.children ?? []
+ const counts = new Map()
+
+ return rootChildren
+ .filter((node) => node.type === "heading" && /^h[1-6]$/.test(node.tag ?? ""))
+ .map((node) => {
+ const text = extractText(node).trim()
+ if (!text) return null
+
+ const base = slugify(text) || "section"
+ const count = counts.get(base) ?? 0
+ counts.set(base, count + 1)
+ const id = count === 0 ? base : `${base}-${count + 1}`
+ const level = Number((node.tag ?? "h2").slice(1)) as 1 | 2 | 3 | 4 | 5 | 6
+
+ return { id, text, level }
+ })
+ .filter((item): item is TocHeading => item !== null)
+}
+
+const injectHeadingIds = (html: string, headings: TocHeading[]): string => {
+ let index = 0
+
+ return html.replace(/]*)>/g, (match, level, attributes) => {
+ const heading = headings[index]
+ index += 1
+ if (!heading) return match
+ if (/\sid=/.test(attributes)) return match
+ return ``
+ })
+}
+
+export async function BlogPost({ slug, locale }: BlogPostProps) {
+ const post = await getBlogPostBySlug(slug, locale)
+ if (!post) notFound()
+
+ const heroImage = post.heroImage as Media
+ const publishedDate = new Intl.DateTimeFormat(locale === "de" ? "de-DE" : "en-US", {
+ dateStyle: "long",
+ }).format(new Date(post.createdAt))
+
+ const headings = getTocHeadings(post.content)
+ const contentHtml = convertLexicalToHTML({
+ data: post.content,
+ disableContainer: true,
+ })
+ const contentHtmlWithIds = injectHeadingIds(contentHtml, headings)
+
+ return (
+
+
+
+
+ {locale === "de" ? "Zurück" : "Back"}
+
+
+
+
+
+ {heroImage?.url ? (
+
+ ) : (
+
+
+
+ {locale === "de" ? "Kein Hero-Bild vorhanden" : "No hero image available"}
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/blog/BlogSkeleton.tsx b/src/components/blog/BlogSkeleton.tsx
new file mode 100644
index 0000000..8d30b82
--- /dev/null
+++ b/src/components/blog/BlogSkeleton.tsx
@@ -0,0 +1,13 @@
+export function BlogSkeleton() {
+ return (
+
+ )
+}
diff --git a/src/components/blog/TableOfContents.tsx b/src/components/blog/TableOfContents.tsx
new file mode 100644
index 0000000..d4de88c
--- /dev/null
+++ b/src/components/blog/TableOfContents.tsx
@@ -0,0 +1,233 @@
+"use client"
+
+import { cn } from "@/lib/utils"
+import { IconAlignLeft, IconChevronDown } from "@tabler/icons-react"
+import { AnimatePresence, m as motion } from "motion/react"
+import { useEffect, useRef, useState } from "react"
+import { useWebHaptics } from "web-haptics/react"
+
+export interface TocHeading {
+ id: string
+ text: string
+ level: 1 | 2 | 3 | 4 | 5 | 6
+}
+
+interface TableOfContentsProps {
+ headings: TocHeading[]
+}
+
+export function TableOfContents({ headings }: TableOfContentsProps) {
+ const { trigger } = useWebHaptics()
+
+ const [activeIds, setActiveIds] = useState([])
+ const [isOpen, setIsOpen] = useState(false)
+ const [isMobile, setIsMobile] = useState(false)
+ const [showMobileToc, setShowMobileToc] = useState(false)
+ const [barStyle, setBarStyle] = useState({ y: 0, scaleY: 0, opacity: 0 })
+
+ const mobileTocRef = useRef(null)
+ const desktopTocRef = useRef(null)
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia("(max-width: 1023px)")
+ const handleMediaChange = (e: MediaQueryListEvent | { matches: boolean }) => {
+ setIsMobile(e.matches)
+ setIsOpen(false)
+ }
+
+ handleMediaChange(mediaQuery)
+ mediaQuery.addEventListener("change", handleMediaChange)
+
+ return () => mediaQuery.removeEventListener("change", handleMediaChange)
+ }, [])
+
+ useEffect(() => {
+ const elements = headings
+ .map((heading) => document.getElementById(heading.id))
+ .filter((el): el is HTMLElement => el !== null)
+
+ if (!elements.length) return
+
+ const visibleIds = new Set()
+ const syncActiveIds = () => {
+ setActiveIds(elements.filter((element) => visibleIds.has(element.id)).map((element) => element.id))
+ }
+ const observer = new IntersectionObserver((entries) => {
+ for (const entry of entries) {
+ if (entry.isIntersecting) visibleIds.add((entry.target as HTMLElement).id)
+ else visibleIds.delete((entry.target as HTMLElement).id)
+ }
+ syncActiveIds()
+ }, {
+ root: null,
+ rootMargin: "-15% 0px -15% 0px",
+ threshold: 0,
+ })
+
+ elements.forEach((element) => observer.observe(element))
+ syncActiveIds()
+
+ return () => observer.disconnect()
+ }, [headings])
+
+ useEffect(() => {
+ const currentRef = isMobile ? mobileTocRef : desktopTocRef
+ if (!currentRef.current) return
+
+ if (!activeIds.length) {
+ setBarStyle((prev) => ({ ...prev, opacity: 0 }))
+ return
+ }
+
+ const listItems = Array.from(currentRef.current.querySelectorAll("[data-toc-item='true']"))
+ const activeElements = activeIds
+ .map((id) => listItems.find((item) => item.id === `toc-${id}`))
+ .filter((el): el is HTMLElement => el !== null && el !== undefined)
+
+ if (!activeElements.length) {
+ setBarStyle((prev) => ({ ...prev, opacity: 0 }))
+ return
+ }
+
+ const sortedActiveElements = activeElements.sort((a, b) => a.offsetTop - b.offsetTop)
+ const firstElement = sortedActiveElements[0]
+ const lastElement = sortedActiveElements[sortedActiveElements.length - 1]
+ const top = firstElement.offsetTop
+ const bottom = lastElement.offsetTop + lastElement.offsetHeight
+ const height = bottom - top
+
+ setBarStyle({ y: top, scaleY: height, opacity: 1 })
+ }, [activeIds, isOpen, isMobile])
+
+ useEffect(() => {
+ if (!isMobile) {
+ setShowMobileToc(true)
+ return
+ }
+
+ const handleScrollVisibility = () => {
+ setShowMobileToc(window.scrollY > 32)
+ setIsOpen(false)
+ }
+
+ handleScrollVisibility()
+ window.addEventListener("scroll", handleScrollVisibility, { passive: true })
+ return () => window.removeEventListener("scroll", handleScrollVisibility)
+ }, [isMobile])
+
+ if (!headings.length) return null
+
+ return (
+ <>
+
+ {showMobileToc && (
+
+
+
+
+
+
+ {isOpen && (
+
+
+
+ {headings.map((heading) => (
+
+ ))}
+
+ )}
+
+
+
+
+ )}
+
+
+
+
+
+ Content
+
+
+
+ {headings.map((heading) => (
+
+ ))}
+
+
+ >
+ )
+}
diff --git a/src/components/cards/ActionListCard.tsx b/src/components/cards/ActionListCard.tsx
new file mode 100644
index 0000000..6a063f7
--- /dev/null
+++ b/src/components/cards/ActionListCard.tsx
@@ -0,0 +1,46 @@
+import { getFeatureBySlug } from "@/lib/cms"
+import { type AppLocale } from "@/lib/i18n"
+import { SiDiscord, SiGithub, SiNotion, SiSap, SiTelegram } from "@icons-pack/react-simple-icons"
+import { OrbitingCircles } from "../animations/OrbitingCircles"
+import { FeatureCardText } from "../FeatureCardText"
+import { FeatureCard } from "./FeatureCard"
+
+interface ActionListCardProps {
+ locale: AppLocale
+ animationDelay?: number
+}
+
+export async function ActionListCard({ locale, animationDelay = 0 }: ActionListCardProps) {
+ const content = await getFeatureBySlug("action-list", locale)
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/cards/BlogCard.tsx b/src/components/cards/BlogCard.tsx
new file mode 100644
index 0000000..252051d
--- /dev/null
+++ b/src/components/cards/BlogCard.tsx
@@ -0,0 +1,61 @@
+"use client"
+
+import { BlogPostItem } from "@/lib/cms"
+import { Media, User } from "@/payload-types"
+import { Card } from "@code0-tech/pictor"
+import Image from "next/image"
+import Link from "next/link"
+import { useWebHaptics } from "web-haptics/react"
+
+export function BlogCard({ locale, post }: { locale: string, post: BlogPostItem }) {
+ const { trigger } = useWebHaptics()
+ const heroImage = post.heroImage as Media
+
+ const publishedDate = new Intl.DateTimeFormat(locale === "de" ? "de-DE" : "en-US", {
+ dateStyle: "long",
+ }).format(new Date(post.createdAt))
+ const authorName = typeof post.author === "number" ? "" : (post.author as User).name
+
+ return (
+ trigger("medium")}
+ className="group block"
+ >
+
+
+
+
+
+
+
+
+
+ {heroImage?.url ? (
+
+
+
+ ) : (
+
+ {locale === "de" ? "Kein Bild" : "No image"}
+
+ )}
+
+
+
{authorName ? `${authorName} - ${publishedDate}` : publishedDate}
+
{post.title}
+ {post.shortDescription ?
{post.shortDescription}
: null}
+
+
+
+
+ )
+}
diff --git a/src/components/cards/ContactCard.tsx b/src/components/cards/ContactCard.tsx
new file mode 100644
index 0000000..2b82e2b
--- /dev/null
+++ b/src/components/cards/ContactCard.tsx
@@ -0,0 +1,152 @@
+"use client"
+
+import { Button, EmailInput, emailValidation, TextAreaInput, TextInput, useForm } from "@code0-tech/pictor"
+import { useMemo, useState } from "react"
+import { useWebHaptics } from "web-haptics/react"
+
+interface ContactCardContent {
+ heading: string
+ nameLabel: string
+ namePlaceholder: string
+ emailLabel: string
+ emailPlaceholder: string
+ messageLabel: string
+ messagePlaceholder: string
+ submitLabel: string
+}
+
+interface ContactCardProps {
+ content?: Partial | null
+}
+
+const defaultContent: ContactCardContent = {
+ heading: "Contact us",
+ nameLabel: "Name",
+ namePlaceholder: "Your name",
+ emailLabel: "Email",
+ emailPlaceholder: "you@example.com",
+ messageLabel: "Message",
+ messagePlaceholder: "How can we help you?",
+ submitLabel: "Send message",
+}
+
+export function ContactCard({ content }: ContactCardProps) {
+ const { trigger } = useWebHaptics()
+ const labels = { ...defaultContent, ...content }
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [submitStatus, setSubmitStatus] = useState<{ type: "success" | "error", message: string } | null>(null)
+ const initialValues = useMemo(
+ () => ({
+ name: "",
+ email: "",
+ message: "",
+ }),
+ [],
+ )
+ const validation = useMemo(
+ () => ({
+ name: (value: string) => {
+ if (!value) return "Name is required"
+ return null
+ },
+ email: (value: string) => {
+ if (!value) return "Email is required"
+ if (!emailValidation(value)) return "Please provide a valid email"
+ return null
+ },
+ message: (value: string) => {
+ if (!value) return "Message is required"
+ return null
+ },
+ }),
+ [],
+ )
+
+ const [inputs, validate] = useForm({
+ useInitialValidation: false,
+ initialValues,
+ validate: validation,
+ onSubmit: (values) => {
+ if (isSubmitting) return
+
+ setIsSubmitting(true)
+ setSubmitStatus(null)
+
+ void (async () => {
+ try {
+ const response = await fetch("/api/contact", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(values),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text().catch(() => "")
+ throw new Error(errorText || "Failed to send message.")
+ }
+
+ inputs.getInputProps("name").formValidation?.setValue("")
+ inputs.getInputProps("email").formValidation?.setValue("")
+ inputs.getInputProps("message").formValidation?.setValue("")
+ setSubmitStatus({ type: "success", message: "Message sent successfully." })
+ } catch (error) {
+ console.error("Contact form submit error:", error)
+ setSubmitStatus({
+ type: "error",
+ message: "Sending failed. Please try again.",
+ })
+ } finally {
+ setIsSubmitting(false)
+ }
+ })()
+ },
+ })
+
+ return (
+
+
{labels.heading}
+
+
+
+
+
+
+
+
+
+
+
+
+ {submitStatus && (
+
+ {submitStatus.message}
+
+ )}
+
+ )
+}
diff --git a/src/components/cards/FeatureCard.tsx b/src/components/cards/FeatureCard.tsx
new file mode 100644
index 0000000..a3f4c4e
--- /dev/null
+++ b/src/components/cards/FeatureCard.tsx
@@ -0,0 +1,101 @@
+"use client"
+
+import { cn } from "@/lib/utils"
+import { ReactNode, useEffect, useRef, useState } from "react"
+
+type FeatureCardTone = "brand" | "aqua" | "blue" | "pink" | "yellow"
+type FeatureCardStyle = {
+ glow?: string
+ border?: string
+ orb?: string
+}
+
+const toneStyles: Record> = {
+ brand: {
+ glow: "from-brand/22 via-brand/8 to-transparent",
+ border: "group-hover:border-brand/28",
+ orb: "bg-brand/18",
+ },
+ aqua: {
+ glow: "from-aqua/22 via-aqua/8 to-transparent",
+ border: "group-hover:border-aqua/28",
+ orb: "bg-aqua/18",
+ },
+ blue: {
+ glow: "from-blue/22 via-blue/8 to-transparent",
+ border: "group-hover:border-blue/28",
+ orb: "bg-blue/18",
+ },
+ pink: {
+ glow: "from-pink/22 via-pink/8 to-transparent",
+ border: "group-hover:border-pink/28",
+ orb: "bg-pink/18",
+ },
+ yellow: {
+ glow: "from-yellow/22 via-yellow/8 to-transparent",
+ border: "group-hover:border-yellow/28",
+ orb: "bg-yellow/18",
+ }
+}
+
+export function FeatureCard({
+ children,
+ className,
+ contentClassName,
+ tone = "brand",
+ style,
+ animationDelay = 0
+}: {
+ children: ReactNode,
+ className?: string,
+ contentClassName?: string,
+ tone?: FeatureCardTone,
+ style?: FeatureCardStyle,
+ animationDelay?: number
+}) {
+ const [isVisible, setIsVisible] = useState(false)
+ const cardRef = useRef(null)
+ const toneStyle = {
+ ...toneStyles[tone],
+ ...style,
+ }
+
+ useEffect(() => {
+ const currentRef = cardRef.current
+ if (!currentRef) return
+
+ const observer = new IntersectionObserver(([entry]) => {
+ if (entry.isIntersecting) {
+ setIsVisible(true)
+ observer.unobserve(currentRef)
+ }
+ }, { rootMargin: "100px" })
+
+ observer.observe(currentRef)
+
+ return () => observer.disconnect()
+ }, [])
+
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+ )
+}
diff --git a/src/components/cards/FirstBlogCard.tsx b/src/components/cards/FirstBlogCard.tsx
new file mode 100644
index 0000000..4e339d2
--- /dev/null
+++ b/src/components/cards/FirstBlogCard.tsx
@@ -0,0 +1,69 @@
+"use client"
+
+import { BlogPostItem } from "@/lib/cms"
+import { Media, User } from "@/payload-types"
+import { Card } from "@code0-tech/pictor"
+import Image from "next/image"
+import Link from "next/link"
+import { useWebHaptics } from "web-haptics/react"
+
+export function FirstBlogCard({ locale, post }: { locale: string, post: BlogPostItem }) {
+ const { trigger } = useWebHaptics()
+ const heroImage = post.heroImage as Media
+
+ const publishedDate = new Intl.DateTimeFormat(locale === "de" ? "de-DE" : "en-US", {
+ dateStyle: "long",
+ }).format(new Date(post.createdAt))
+
+ const authorName = typeof post.author === "number" ? "" : (post.author as User).name
+
+ return (
+ trigger("medium")}
+ className="group block"
+ >
+
+
+
+
+
+
+
+
+
+ {heroImage?.url ? (
+
+
+
+ ) : (
+
+ {locale === "de" ? "Kein Bild" : "No image"}
+
+ )}
+
+
+
+ {authorName ? `${authorName} - ${publishedDate}` : publishedDate}
+
+
{post.title}
+ {post.shortDescription ? (
+
+ {post.shortDescription}
+
+ ) : null}
+
+
+
+
+ )
+}
diff --git a/src/components/cards/JobApplicationCard.tsx b/src/components/cards/JobApplicationCard.tsx
new file mode 100644
index 0000000..1c52956
--- /dev/null
+++ b/src/components/cards/JobApplicationCard.tsx
@@ -0,0 +1,153 @@
+"use client"
+
+import { Button, EmailInput, emailValidation, TextAreaInput, TextInput, useForm } from "@code0-tech/pictor"
+import { useMemo, useState } from "react"
+import { useWebHaptics } from "web-haptics/react"
+
+interface JobApplicationCardContent {
+ applicationHeading: string
+ applicationNameLabel: string
+ applicationNamePlaceholder: string
+ applicationEmailLabel: string
+ applicationEmailPlaceholder: string
+ applicationMessageLabel: string
+ applicationMessagePlaceholder: string
+ applicationSubmitLabel: string
+}
+
+interface JobApplicationCardProps {
+ jobSlug: string
+ content?: Partial | null
+}
+
+const defaultContent: JobApplicationCardContent = {
+ applicationHeading: "Apply now",
+ applicationNameLabel: "Name",
+ applicationNamePlaceholder: "Your name",
+ applicationEmailLabel: "Email",
+ applicationEmailPlaceholder: "you@example.com",
+ applicationMessageLabel: "Message",
+ applicationMessagePlaceholder: "Tell us a bit about yourself...",
+ applicationSubmitLabel: "Send application",
+}
+
+export function JobApplicationCard({ jobSlug, content }: JobApplicationCardProps) {
+ const { trigger } = useWebHaptics()
+ const labels = { ...defaultContent, ...content }
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [submitStatus, setSubmitStatus] = useState<{ type: "success" | "error", message: string } | null>(null)
+ const initialValues = useMemo(
+ () => ({
+ name: "",
+ email: "",
+ text: "",
+ }),
+ [],
+ )
+ const validation = useMemo(
+ () => ({
+ name: (value: string) => {
+ if (!value) return "Name is required"
+ return null
+ },
+ email: (value: string) => {
+ if (!value) return "Email is required"
+ if (!emailValidation(value)) return "Please provide a valid email"
+ return null
+ },
+ text: (value: string) => {
+ if (!value) return "Message is required"
+ return null
+ },
+ }),
+ [],
+ )
+
+ const [inputs, validate] = useForm({
+ useInitialValidation: false,
+ initialValues,
+ validate: validation,
+ onSubmit: (values) => {
+ if (isSubmitting) return
+
+ setIsSubmitting(true)
+ setSubmitStatus(null)
+
+ void (async () => {
+ try {
+ const response = await fetch(`/api/jobs/${encodeURIComponent(jobSlug)}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(values),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text().catch(() => "")
+ throw new Error(errorText || "Failed to send application.")
+ }
+
+ inputs.getInputProps("name").formValidation?.setValue("")
+ inputs.getInputProps("email").formValidation?.setValue("")
+ inputs.getInputProps("text").formValidation?.setValue("")
+ setSubmitStatus({ type: "success", message: "Application sent successfully." })
+ } catch (error) {
+ console.error("Job application submit error:", error)
+ setSubmitStatus({
+ type: "error",
+ message: "Sending failed. Please try again.",
+ })
+ } finally {
+ setIsSubmitting(false)
+ }
+ })()
+ },
+ })
+
+ return (
+
+
{labels.applicationHeading}
+
+
+
+
+
+
+
+
+
+
+
+
+ {submitStatus && (
+
+ {submitStatus.message}
+
+ )}
+
+ )
+}
diff --git a/src/components/cards/JobsCard.tsx b/src/components/cards/JobsCard.tsx
new file mode 100644
index 0000000..10fa3a2
--- /dev/null
+++ b/src/components/cards/JobsCard.tsx
@@ -0,0 +1,43 @@
+"use client"
+
+import type { JobItem } from "@/lib/cms"
+import { Card } from "@code0-tech/pictor"
+import Link from "next/link"
+import { useWebHaptics } from "web-haptics/react"
+
+interface JobsCardProps {
+ job: JobItem
+ locale: string
+}
+
+export function JobsCard({ job, locale }: JobsCardProps) {
+ const { trigger } = useWebHaptics()
+
+ return (
+ trigger("medium")}
+ className="group block"
+ >
+
+
+
+
+
+
+
+
+
+
{job.title}
+
+ {job.location} - {job.type}
+
+
{job.description}
+
+
+
+ )
+}
diff --git a/src/components/cards/MemberManagementCard.tsx b/src/components/cards/MemberManagementCard.tsx
new file mode 100644
index 0000000..17bc2ce
--- /dev/null
+++ b/src/components/cards/MemberManagementCard.tsx
@@ -0,0 +1,28 @@
+import { getFeatureBySlug } from "@/lib/cms"
+import { type AppLocale } from "@/lib/i18n"
+import { ClientMemberCard } from "../ClientMemberCard"
+import { FeatureCardText } from "../FeatureCardText"
+import { FeatureCard } from "./FeatureCard"
+
+interface MemberMangementCardProps {
+ locale: AppLocale
+ animationDelay?: number
+}
+
+export async function MemberManagementCard({ locale, animationDelay = 0 }: MemberMangementCardProps) {
+ const content = await getFeatureBySlug("member-management", locale)
+
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/cards/NodeCard.tsx b/src/components/cards/NodeCard.tsx
new file mode 100644
index 0000000..b35b711
--- /dev/null
+++ b/src/components/cards/NodeCard.tsx
@@ -0,0 +1,37 @@
+import { getFeatureBySlug } from "@/lib/cms"
+import { type AppLocale } from "@/lib/i18n"
+import { FeatureCardText } from "../FeatureCardText"
+import { FeatureCard } from "./FeatureCard"
+import { NodesAnimation } from "../animations/NodesAnimation"
+
+interface NodeTabsCardProps {
+ locale: AppLocale
+ animationDelay?: number
+}
+
+export async function NodeCard({ locale, animationDelay = 0 }: NodeTabsCardProps) {
+ const content = await getFeatureBySlug("nodes", locale)
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/components/cards/OrganizationCard.tsx b/src/components/cards/OrganizationCard.tsx
new file mode 100644
index 0000000..0465089
--- /dev/null
+++ b/src/components/cards/OrganizationCard.tsx
@@ -0,0 +1,30 @@
+import { type AppLocale } from "@/lib/i18n"
+import { getFeatureBySlug } from "@/lib/cms"
+import { FeatureCard } from "./FeatureCard"
+import { FeatureCardText } from "../FeatureCardText"
+import { OrganizationsDataTable } from "../tables/OrganizationsDataTable"
+
+interface OrganizationCardProps {
+ locale: AppLocale
+ animationDelay?: number
+}
+
+export async function OrganizationCard({ locale, animationDelay = 0 }: OrganizationCardProps) {
+ const content = await getFeatureBySlug("organizations", locale)
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/cards/ProjectsCard.tsx b/src/components/cards/ProjectsCard.tsx
new file mode 100644
index 0000000..26b49a6
--- /dev/null
+++ b/src/components/cards/ProjectsCard.tsx
@@ -0,0 +1,29 @@
+import { getFeatureBySlug } from "@/lib/cms"
+import { type AppLocale } from "@/lib/i18n"
+import { FeatureCardText } from "../FeatureCardText"
+import { ProjectDataTable } from "../tables/ProjectDataTable"
+import { FeatureCard } from "./FeatureCard"
+
+interface ProjectsCardProps {
+ locale: AppLocale
+ animationDelay?: number
+}
+
+export async function ProjectsCard({ locale, animationDelay = 0 }: ProjectsCardProps) {
+ const content = await getFeatureBySlug("projects", locale)
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/components/cards/RoleSystemCard.tsx b/src/components/cards/RoleSystemCard.tsx
new file mode 100644
index 0000000..9b74a5c
--- /dev/null
+++ b/src/components/cards/RoleSystemCard.tsx
@@ -0,0 +1,62 @@
+import { getFeatureBySlug } from "@/lib/cms"
+import { type AppLocale } from "@/lib/i18n"
+import { FeatureCardText } from "../FeatureCardText"
+import { FeatureCard } from "./FeatureCard"
+import { RoleSystemAnimation } from "../animations/RoleSystemAnimation"
+
+interface RoleSystemCardProps {
+ locale: AppLocale
+ animationDelay?: number
+}
+
+export async function RoleSystemCard({ locale, animationDelay = 0 }: RoleSystemCardProps) {
+ const content = await getFeatureBySlug("role-system", locale)
+
+ const roles = [
+ {
+ name: "Owner",
+ description: "Can manage",
+ badges: ["everything"],
+ updatedAt: "Updated 16 days ago",
+ },
+ {
+ name: "Maintainer",
+ description: "Can manage",
+ badges: ["projects", "organization", "runtimes"],
+ updatedAt: "Updated 16 days ago",
+ },
+ {
+ name: "Member",
+ description: "Can manage",
+ badges: ["flows", "projects"],
+ updatedAt: "Updated 16 days ago",
+ },
+ {
+ name: "Test",
+ description: "Can manage",
+ badges: ["projects", "roles", "flows",],
+ updatedAt: "Updated 4 days ago",
+ },
+ ]
+
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/cards/RuntimeTypesCard.tsx b/src/components/cards/RuntimeTypesCard.tsx
new file mode 100644
index 0000000..8e3c554
--- /dev/null
+++ b/src/components/cards/RuntimeTypesCard.tsx
@@ -0,0 +1,33 @@
+import { getFeatureBySlug } from "@/lib/cms"
+import { type AppLocale } from "@/lib/i18n"
+import { FeatureCardText } from "../FeatureCardText"
+import { FeatureCard } from "./FeatureCard"
+import { RuntimeControlClient } from "../RuntimeControlClient"
+
+interface RuntimeTypesCardProps {
+ locale: AppLocale
+ animationDelay?: number
+}
+
+export async function RuntimeTypesCard({ locale, animationDelay = 0 }: RuntimeTypesCardProps) {
+ const content = await getFeatureBySlug("runtime-types", locale)
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/components/cards/SuggestionMenuCard.tsx b/src/components/cards/SuggestionMenuCard.tsx
new file mode 100644
index 0000000..773d128
--- /dev/null
+++ b/src/components/cards/SuggestionMenuCard.tsx
@@ -0,0 +1,31 @@
+import { getFeatureBySlug } from "@/lib/cms"
+import { type AppLocale } from "@/lib/i18n"
+import { FeatureCardText } from "../FeatureCardText"
+import { SuggesstionMenuClient } from "../ui/SuggesstionMenuClient"
+import { FeatureCard } from "./FeatureCard"
+
+interface SuggestionMenuCardProps {
+ locale: AppLocale
+ animationDelay?: number
+}
+
+export async function SuggestionMenuCard({ locale, animationDelay = 0 }: SuggestionMenuCardProps) {
+ const content = await getFeatureBySlug("suggestion-menu", locale)
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/components/cards/TeamMemberCard.tsx b/src/components/cards/TeamMemberCard.tsx
new file mode 100644
index 0000000..1672a04
--- /dev/null
+++ b/src/components/cards/TeamMemberCard.tsx
@@ -0,0 +1,158 @@
+"use client"
+
+import type { TeamMemberItem } from "@/lib/cms"
+import type { Media } from "@/payload-types"
+import { Card } from "@code0-tech/pictor"
+import { IconX } from "@tabler/icons-react"
+import { AnimatePresence, m as motion } from "motion/react"
+import Image from "next/image"
+import { useState } from "react"
+import { useWebHaptics } from "web-haptics/react"
+
+interface TeamMemberCardProps {
+ member: TeamMemberItem
+ locale: string
+}
+
+function getInitials(name: string): string {
+ const parts = name.trim().split(/\s+/).slice(0, 2)
+ return parts.map((part) => part.charAt(0).toUpperCase()).join("")
+}
+
+export function TeamMemberCard({ member, locale }: TeamMemberCardProps) {
+ const [isOpen, setIsOpen] = useState(false)
+ const { trigger } = useWebHaptics()
+
+ const image = member.image as Media
+ const cardLayoutId = `team-member-card-${member.id ?? member.name}`
+ const joinedAtLabel = member.joinedAt
+ ? new Intl.DateTimeFormat(locale === "de" ? "de-DE" : "en-US", { dateStyle: "medium" }).format(new Date(member.joinedAt))
+ : null
+ const joinedLabel = locale === "de" ? "Beigetreten" : "Joined"
+
+ return (
+ <>
+ {
+ trigger("medium")
+ setIsOpen(true)
+ }}
+ onKeyDown={(event) => {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault()
+ trigger("medium")
+ setIsOpen(true)
+ }
+ }}
+ role="button"
+ tabIndex={0}
+ aria-haspopup="dialog"
+ aria-label={`${member.name} details`}
+ whileTap={{ scale: 0.98 }}
+ >
+
+
+
+
+
+
+
+
+
+
+ {image?.url ? (
+
+ ) : (
+
+ {getInitials(member.name)}
+
+ )}
+
+
{member.name}
+ {member.role ?
{member.role}
: null}
+
+
+
+ {member.shortDescription ?
{member.shortDescription}
: null}
+ {joinedAtLabel ?
{joinedLabel}: {joinedAtLabel}
: null}
+
+
+
+
+
+ {isOpen ? (
+ setIsOpen(false)}
+ >
+ event.stopPropagation()}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {image?.url ? (
+
+ ) : (
+
+ {getInitials(member.name)}
+
+ )}
+
+
{member.name}
+ {member.role ?
{member.role}
: null}
+
+
+
+
+
+ {member.about ?
{member.about}
: null}
+ {joinedAtLabel ?
{joinedLabel}: {joinedAtLabel}
: null}
+
+
+
+
+ ) : null}
+
+ >
+ )
+}
diff --git a/src/components/navigation/NavSubMenu.tsx b/src/components/navigation/NavSubMenu.tsx
new file mode 100644
index 0000000..36345be
--- /dev/null
+++ b/src/components/navigation/NavSubMenu.tsx
@@ -0,0 +1,45 @@
+"use client"
+
+import { cn } from "@/lib/utils"
+import { m as motion } from "motion/react"
+import Link from "next/link"
+import React from "react"
+import { SubNavItem } from "./types"
+
+type NavSubMenuProps = {
+ items: SubNavItem[]
+ onSelect?: (item: SubNavItem) => void
+ variant?: "overlay" | "inline"
+}
+
+const NavSubMenu: React.FC = ({ items, onSelect, variant = "overlay" }) => {
+ return (
+
+ {items.map((subItem, index) => (
+
+ onSelect?.(subItem)}
+ className="group h-14 flex items-center p-2 hover:bg-white/10 cursor-pointer gap-2 rounded-lg">
+
+ {subItem.icon}
+
+
+
{subItem.title}
+
{subItem.description}
+
+
+
+ ))}
+
+ )
+}
+
+export { NavSubMenu }
diff --git a/src/components/navigation/NavTab.tsx b/src/components/navigation/NavTab.tsx
new file mode 100644
index 0000000..e88e06a
--- /dev/null
+++ b/src/components/navigation/NavTab.tsx
@@ -0,0 +1,75 @@
+"use client"
+
+import { cn } from "@/lib/utils"
+import { IconChevronUp } from "@tabler/icons-react"
+import { m as motion } from "motion/react"
+import Link from "next/link"
+import React, { useRef } from "react"
+import { fadeInUp, SubNavItem } from "./types"
+
+type TabProps = {
+ setPosition: React.Dispatch>
+ href: string | null
+ subMenu?: SubNavItem[]
+ activeSubMenu?: SubNavItem[] | null
+ onMouseEnter: () => void
+ title: string
+}
+
+const NavTab: React.FC = ({ setPosition, href, title, subMenu, activeSubMenu, onMouseEnter }) => {
+ const ref = useRef(null)
+ const hasSubMenu = Boolean(subMenu?.length)
+ const active = activeSubMenu && activeSubMenu === subMenu
+ const interactiveClassName = cn(
+ "relative z-50 flex items-center gap-2 px-4 py-1 font-medium text-md rounded-xl cursor-pointer",
+ hasSubMenu && "pr-1"
+ )
+
+ return (
+ {
+ if (!ref?.current) return
+
+ const { width } = ref.current.getBoundingClientRect()
+
+ setPosition({
+ left: ref.current.offsetLeft,
+ width,
+ opacity: 1
+ })
+ onMouseEnter()
+ }}
+ >
+ {href ? (
+
+ {title}
+ {hasSubMenu && (
+ active ? (
+
+ ) : (
+
+ )
+ )}
+
+ ) : (
+
+ )}
+
+ )
+}
+
+export { NavTab }
diff --git a/src/components/navigation/Navigation.tsx b/src/components/navigation/Navigation.tsx
new file mode 100644
index 0000000..08a39b7
--- /dev/null
+++ b/src/components/navigation/Navigation.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import type { NavbarItem } from "@/payload-types"
+import { useMediaQuery } from "@/hooks/useMediaQuery"
+import { useOutsideClick } from "@/hooks/useOutsideClick"
+import { localizeHref, type AppLocale } from "@/lib/i18n"
+import { IconCube, IconGitBranch, IconLock } from "@tabler/icons-react"
+import { useEffect, useMemo, useState } from "react"
+import { NavigationDesktop } from "./NavigationDesktop"
+import { NavigationMobile } from "./NavigationMobile"
+import { SubNavItem } from "./types"
+
+type SubMenuIcon = "cube" | "gitBranch" | "lock"
+
+interface NavigationProps {
+ locale: AppLocale
+ items: NavbarItem[]
+}
+
+function Navigation({ locale, items }: NavigationProps) {
+ const scrollOpenThreshold = 8
+ const scrollCloseThreshold = 3
+ const isDesktop = useMediaQuery("(min-width: 1024px)")
+ const menuRef = useOutsideClick(() => setIsOpen(false))
+ const subMenuRef = useOutsideClick(() => setActiveSubMenu(null))
+
+ const [position, setPosition] = useState({ left: 0, width: 0, opacity: 0 })
+ const [isScrolled, setIsScrolled] = useState(false)
+ const [isOpen, setIsOpen] = useState(false)
+ const [activeSubMenu, setActiveSubMenu] = useState(null)
+ const [hoveredSubMenu, setHoveredSubMenu] = useState(null)
+ const [mobileOpenKey, setMobileOpenKey] = useState(null)
+ const homeHref = `/${locale}`
+
+ const navbarItems = useMemo(() => {
+ const getSubMenuIcon = (icon: string | null | undefined) => {
+ if (icon === "cube") return
+ if (icon === "gitBranch") return
+ if (icon === "lock") return
+ return null
+ }
+
+ return items.map((item) => {
+ const mappedSubMenu = (item.subMenu ?? [])
+ .filter((sub) => Boolean(sub?.title && sub?.href && sub?.description))
+ .map((sub) => ({
+ ...sub,
+ icon: getSubMenuIcon((sub.icon as SubMenuIcon | null | undefined) ?? null),
+ color: sub.color ?? "brand",
+ }))
+
+ return {
+ title: item.title,
+ href: item.href ? localizeHref(item.href, locale) : null,
+ subMenu: mappedSubMenu.length > 0
+ ? mappedSubMenu.map((sub) => ({ ...sub, href: localizeHref(sub.href, locale) }))
+ : undefined,
+ }
+ })
+ }, [items, locale])
+
+ useEffect(() => {
+ const handleScroll = () => {
+ setIsScrolled((prevIsScrolled) => {
+ const nextIsScrolled = prevIsScrolled
+ ? window.scrollY > scrollCloseThreshold
+ : window.scrollY > scrollOpenThreshold
+
+ if (prevIsScrolled !== nextIsScrolled && nextIsScrolled) {
+ setActiveSubMenu(null)
+ }
+
+ return nextIsScrolled
+ })
+ setIsOpen(false)
+ }
+
+ if (window.scrollY > scrollOpenThreshold) {
+ setIsScrolled(true)
+ }
+ window.addEventListener("scroll", handleScroll)
+
+ return () => window.removeEventListener("scroll", handleScroll)
+ }, [scrollCloseThreshold, scrollOpenThreshold])
+
+ useEffect(() => {
+ if (!isScrolled && hoveredSubMenu) {
+ setActiveSubMenu(hoveredSubMenu)
+ }
+ }, [hoveredSubMenu, isScrolled])
+
+ if (!isDesktop) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+export { Navigation }
diff --git a/src/components/navigation/NavigationDesktop.tsx b/src/components/navigation/NavigationDesktop.tsx
new file mode 100644
index 0000000..e64b344
--- /dev/null
+++ b/src/components/navigation/NavigationDesktop.tsx
@@ -0,0 +1,170 @@
+"use client"
+
+import { cn } from "@/lib/utils"
+import { DEFAULT_DISCORD_URL, DEFAULT_GITHUB_URL } from "@/lib/siteConfig"
+import { Button, Container } from "@code0-tech/pictor"
+import { SiDiscord, SiGithub } from "@icons-pack/react-simple-icons"
+import { AnimatePresence, m as motion } from "motion/react"
+import Image from "next/image"
+import Link from "next/link"
+import React from "react"
+import { NavSubMenu } from "./NavSubMenu"
+import { NavTab } from "./NavTab"
+import { fadeInUp, NavItem, SubNavItem } from "./types"
+
+type NavigationDesktopProps = {
+ isScrolled: boolean
+ navbarItems: NavItem[]
+ position: { left: number; width: number; opacity: number }
+ setPosition: React.Dispatch>
+ activeSubMenu: SubNavItem[] | null
+ setActiveSubMenu: React.Dispatch>
+ setHoveredSubMenu: React.Dispatch>
+ subMenuRef: React.RefObject
+ homeHref: string
+}
+
+const NavigationDesktop: React.FC = ({
+ isScrolled,
+ navbarItems,
+ position,
+ setPosition,
+ activeSubMenu,
+ setActiveSubMenu,
+ setHoveredSubMenu,
+ subMenuRef,
+ homeHref,
+}) => {
+ return (
+
+
+ {
+ setPosition({ left: position.left, width: position.width, opacity: 0 })
+ setActiveSubMenu(null)
+ setHoveredSubMenu(null)
+ }}
+ initial={{
+ marginLeft: "0%",
+ marginRight: "0%",
+ }}
+ animate={{
+ marginLeft: isScrolled ? "10%" : "0%",
+ marginRight: isScrolled ? "10%" : "0%"
+ }}
+ transition={{
+ type: "spring",
+ stiffness: 40,
+ damping: 10,
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ {navbarItems.map((item) => (
+ {
+ setActiveSubMenu(item.subMenu || null)
+ setHoveredSubMenu(item.subMenu || null)
+ }}
+ />
+ ))}
+
+
+ {activeSubMenu && !isScrolled && (
+
+
+ {
+ setActiveSubMenu(null)
+ setHoveredSubMenu(null)
+ }}
+ variant="overlay"
+ />
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {activeSubMenu && isScrolled && (
+
+ {
+ setActiveSubMenu(null)
+ setHoveredSubMenu(null)
+ }}
+ variant="inline"
+ />
+
+ )}
+
+
+
+
+ )
+}
+
+const Cursor: React.FC<{ position: {left: number, width: number, opacity: number} }> = ({ position }) => {
+ return (
+
+ )
+}
+
+export { NavigationDesktop }
diff --git a/src/components/navigation/NavigationMobile.tsx b/src/components/navigation/NavigationMobile.tsx
new file mode 100644
index 0000000..4d61711
--- /dev/null
+++ b/src/components/navigation/NavigationMobile.tsx
@@ -0,0 +1,261 @@
+"use client"
+
+import { cn } from "@/lib/utils"
+import { DEFAULT_DISCORD_URL, DEFAULT_GITHUB_URL } from "@/lib/siteConfig"
+import { Button, Container } from "@code0-tech/pictor"
+import { SiGithub } from '@icons-pack/react-simple-icons'
+import { IconChevronUp, IconMenu2, IconX } from "@tabler/icons-react"
+import { AnimatePresence, m as motion } from "motion/react"
+import Image from "next/image"
+import Link from "next/link"
+import React from "react"
+import { useWebHaptics } from "web-haptics/react"
+import { fadeInUp, NavItem } from "./types"
+
+type NavigationMobileProps = {
+ menuRef: React.RefObject
+ isScrolled: boolean
+ isOpen: boolean
+ setIsOpen: React.Dispatch>
+ navbarItems: NavItem[]
+ mobileOpenKey: string | null
+ setMobileOpenKey: React.Dispatch>
+ homeHref: string
+}
+
+const NavigationMobile: React.FC = ({
+ menuRef,
+ isScrolled,
+ isOpen,
+ setIsOpen,
+ navbarItems,
+ mobileOpenKey,
+ setMobileOpenKey,
+ homeHref,
+}) => {
+ const { trigger } = useWebHaptics()
+
+ return (
+
+
+
+
+ {
+ trigger("medium")
+ setIsOpen(false)
+ }}
+ >
+
+
+
+
+ {
+ trigger("medium")
+ setIsOpen(!isOpen)
+ }}
+ >
+ {isOpen ? : }
+
+
+
+ {isOpen && (
+
+ {navbarItems.map((item, i) => {
+ const isAccordion = !!item.subMenu?.length
+ const isOpenAcc = mobileOpenKey === item.title
+
+ const hasRoute = Boolean(item.href)
+ return (
+
+
+ {isAccordion ? (
+
+ ) : (
+ {
+ trigger("medium")
+ setIsOpen(false)
+ }}
+ >
+ {item.title}
+
+ )}
+
+
+ {isAccordion && (
+
+ {isOpenAcc && (
+
+
+ {item.subMenu!.map((sub) => (
+
{
+ trigger("medium")
+ setIsOpen(false)
+ setMobileOpenKey(null)
+ }}
+ >
+
+ {sub.icon}
+
+
+ {sub.title}
+ {sub.description}
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+ )
+ })}
+
+
+ {
+ trigger("medium")
+ setIsOpen(false)
+ }}
+ >
+
+
+
+
+ {
+ trigger("medium")
+ setIsOpen(false)
+ }}
+ >
+
+
+
+
+
+ )}
+
+
+
+
+ )
+}
+
+export { NavigationMobile }
diff --git a/src/components/navigation/types.ts b/src/components/navigation/types.ts
new file mode 100644
index 0000000..161c68f
--- /dev/null
+++ b/src/components/navigation/types.ts
@@ -0,0 +1,22 @@
+import {ReactNode} from "react"
+
+export type NavItem = {
+ title: string
+ href: string | null
+ subMenu?: SubNavItem[]
+}
+
+export type SubNavItem = {
+ key: string
+ title: string
+ href: string
+ description: string
+ icon: ReactNode
+ color: string
+}
+
+export const fadeInUp = {
+ initial: { opacity: 0, y: -16 },
+ animate: { opacity: 1, y: 0 },
+ transition: {duration: 0.65}
+}
diff --git a/src/components/pages/AboutUsPageClient.tsx b/src/components/pages/AboutUsPageClient.tsx
new file mode 100644
index 0000000..1df4116
--- /dev/null
+++ b/src/components/pages/AboutUsPageClient.tsx
@@ -0,0 +1,24 @@
+import { MarkdownContent } from "@/components/MarkdownContent"
+import { TeamMemberCard } from "@/components/cards/TeamMemberCard"
+import { getTeamMembers } from "@/lib/cms"
+
+interface AboutUsPageClientProps {
+ locale: string
+ content: string
+}
+
+export async function AboutUsPageClient({ locale, content }: AboutUsPageClientProps) {
+ const teamMembers = await getTeamMembers()
+
+ return (
+
+
+
Team
+
+ {teamMembers.map((member) => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/pages/JobsPageClient.tsx b/src/components/pages/JobsPageClient.tsx
new file mode 100644
index 0000000..91991bc
--- /dev/null
+++ b/src/components/pages/JobsPageClient.tsx
@@ -0,0 +1,159 @@
+"use client"
+
+import { JobsCard } from "@/components/cards/JobsCard"
+import type { JobItem } from "@/lib/cms"
+import {
+ Button,
+ Menu,
+ MenuContent,
+ MenuItem,
+ MenuTrigger,
+ TextInput
+} from "@code0-tech/pictor"
+import { IconChevronDown, IconSearch } from "@tabler/icons-react"
+import { useMemo, useState } from "react"
+
+interface JobsPageContent {
+ heading: string
+ searchPlaceholder: string
+ allLocationsLabel: string
+ allJobTypesLabel: string
+ allCategoriesLabel: string
+ noJobsFoundLabel: string
+}
+
+interface JobsPageClientProps {
+ jobs: JobItem[]
+ locale: string
+ content?: Partial | null
+}
+
+const defaultContent: JobsPageContent = {
+ heading: "Join Our Team",
+ searchPlaceholder: "Search jobs",
+ allLocationsLabel: "All locations",
+ allJobTypesLabel: "All job types",
+ allCategoriesLabel: "All categories",
+ noJobsFoundLabel: "No jobs found for your filter.",
+}
+
+export function JobsPageClient({ jobs, locale, content }: JobsPageClientProps) {
+ const labels = { ...defaultContent, ...content }
+ const [search, setSearch] = useState("")
+ const [selectedLocation, setSelectedLocation] = useState(labels.allLocationsLabel)
+ const [selectedType, setSelectedType] = useState(labels.allJobTypesLabel)
+ const [selectedCategory, setSelectedCategory] = useState(labels.allCategoriesLabel)
+
+ const locations = useMemo(() => [labels.allLocationsLabel, ...Array.from(new Set(jobs.map((job) => job.location)))], [jobs, labels.allLocationsLabel])
+ const jobTypes = useMemo(() => [labels.allJobTypesLabel, ...Array.from(new Set(jobs.map((job) => job.type)))], [jobs, labels.allJobTypesLabel])
+ const categories = useMemo(() => [labels.allCategoriesLabel, ...Array.from(new Set(jobs.map((job) => job.category)))], [jobs, labels.allCategoriesLabel])
+
+ const filteredJobs = useMemo(() => {
+ const searchTerm = search.trim().toLowerCase()
+
+ return jobs.filter((job) => {
+ const matchesSearch =
+ searchTerm.length === 0 ||
+ `${job.title} ${job.location} ${job.description}`
+ .toLowerCase()
+ .includes(searchTerm)
+
+ const matchesLocation = selectedLocation === labels.allLocationsLabel || job.location === selectedLocation
+ const matchesType = selectedType === labels.allJobTypesLabel || job.type === selectedType
+ const matchesCategory = selectedCategory === labels.allCategoriesLabel || job.category === selectedCategory
+
+ return matchesSearch && matchesLocation && matchesType && matchesCategory
+ })
+ }, [jobs, labels.allCategoriesLabel, labels.allJobTypesLabel, labels.allLocationsLabel, search, selectedCategory, selectedLocation, selectedType])
+
+ const groupedJobs = useMemo(() => {
+ return filteredJobs.reduce>((acc, job) => {
+ if (!acc[job.category]) acc[job.category] = []
+ acc[job.category].push(job)
+ return acc
+ }, {})
+ }, [filteredJobs])
+
+ return (
+
+
{labels.heading}
+
+
+
setSearch(event.target.value)}
+ placeholder={labels.searchPlaceholder}
+ left={[]}
+ clearable
+ className="w-full rounded-xl bg-white/10 border border-white/15 text-white/85"
+ />
+
+
+
+
+
+
+
+
+
+
+ {Object.entries(groupedJobs).map(([category, items]) => (
+
+
+ {items.map((job) => (
+
+ ))}
+
+ ))}
+
+ {filteredJobs.length === 0 && (
+
{labels.noJobsFoundLabel}
+ )}
+
+ )
+}
diff --git a/src/components/providers/MotionProvider.tsx b/src/components/providers/MotionProvider.tsx
new file mode 100644
index 0000000..20dab07
--- /dev/null
+++ b/src/components/providers/MotionProvider.tsx
@@ -0,0 +1,14 @@
+"use client"
+
+import { LazyMotion, MotionConfig, domAnimation } from "motion/react"
+import type { ReactNode } from "react"
+
+export function MotionProvider({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/src/components/providers/SectionsProvider.tsx b/src/components/providers/SectionsProvider.tsx
new file mode 100644
index 0000000..24a870c
--- /dev/null
+++ b/src/components/providers/SectionsProvider.tsx
@@ -0,0 +1,35 @@
+"use client"
+
+import type { Section } from "@/payload-types"
+import { createContext, type ReactNode, useContext, useMemo } from "react"
+
+type SectionType = NonNullable
+type SectionsMap = Partial>
+
+const SectionsContext = createContext({})
+
+interface SectionsProviderProps {
+ sections: Section[]
+ children: ReactNode
+}
+
+export function SectionsProvider({ sections, children }: SectionsProviderProps) {
+ const sectionsMap = useMemo(() => {
+ return sections.reduce((acc, section) => {
+ acc[section.sectionType] = section
+ return acc
+ }, {})
+ }, [sections])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function usePreloadedSection(sectionType?: SectionType) {
+ const sections = useContext(SectionsContext)
+ if (!sectionType) return null
+ return sections[sectionType] ?? null
+}
diff --git a/src/components/sections/AppFeatureSection.tsx b/src/components/sections/AppFeatureSection.tsx
new file mode 100644
index 0000000..e2c952e
--- /dev/null
+++ b/src/components/sections/AppFeatureSection.tsx
@@ -0,0 +1,24 @@
+import { Section } from "@/components/ui/Section"
+import { type AppLocale } from "@/lib/i18n"
+import { MemberManagementCard } from "../cards/MemberManagementCard"
+import { OrganizationCard } from "../cards/OrganizationCard"
+import { RoleSystemCard } from "../cards/RoleSystemCard"
+import { ProjectsCard } from "../cards/ProjectsCard"
+import { BentoGrid } from "../ui/BentoGrid"
+
+interface AppFeatureSectionProps {
+ locale: AppLocale
+}
+
+export const AppFeatureSection: React.FC = ({ locale }) => {
+ return (
+
+ )
+}
diff --git a/src/components/sections/BrandSection.tsx b/src/components/sections/BrandSection.tsx
new file mode 100644
index 0000000..0e63d28
--- /dev/null
+++ b/src/components/sections/BrandSection.tsx
@@ -0,0 +1,103 @@
+"use client"
+
+import React from "react"
+import { Section } from "@/components/ui/Section"
+import Image from "next/image"
+import type { Media } from "@/payload-types"
+import { m as motion, type Variants } from "motion/react"
+
+interface BrandSectionLogo {
+ logo: number | Media
+ id?: string | null
+}
+
+interface BrandSectionContent {
+ description?: string | null
+ logos?: BrandSectionLogo[] | null
+}
+
+interface BrandSectionProps {
+ content?: BrandSectionContent | null
+}
+
+export const BrandSection: React.FC = ({ content }) => {
+ if (!content) return
+
+ const logos = (content.logos ?? [])
+ .map((item) => item.logo)
+ .filter((logo) => Boolean((logo as Media)?.url))
+
+ const staggerContainer: Variants = {
+ hidden: {},
+ show: {
+ transition: {
+ staggerChildren: 0.08,
+ delayChildren: 0.06,
+ },
+ },
+ }
+
+ const staggerItem: Variants = {
+ hidden: { opacity: 0, y: 14 },
+ show: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.38, ease: [0.22, 1, 0.36, 1] },
+ },
+ }
+
+ return (
+
+
+
+ {content.description}
+
+
+ {logos.length > 0 ? (
+ logos.map((item, index) => {
+ const href = (item as Media & { href?: string | null }).href
+ const logo = item as Media
+
+ return (
+
+ {href ? (
+
+
+
+ ) : (
+
+ )}
+
+ )
+ })
+ ) : (
+ <>
+ Logo1
+ Logo2
+ Logo3
+ Logo4
+ >
+ )}
+
+
+
+ )
+}
diff --git a/src/components/sections/CtaSection.tsx b/src/components/sections/CtaSection.tsx
new file mode 100644
index 0000000..a3cc14e
--- /dev/null
+++ b/src/components/sections/CtaSection.tsx
@@ -0,0 +1,102 @@
+"use client"
+
+import { InteractiveGridPattern } from "@/components/InteractiveGridPattern"
+import { Section } from "@/components/ui/Section"
+import { cn } from "@/lib/utils"
+import Image from "next/image"
+import React from "react"
+import Link from "next/link"
+import { Button } from "@code0-tech/pictor"
+import { m as motion, type Variants } from "motion/react"
+import { useWebHaptics } from "web-haptics/react"
+
+interface CtaSectionContent {
+ heading: string
+ subheading: string
+ ctaLink: {
+ label: string
+ url: string
+ }
+}
+
+interface CtaSectionProps {
+ content?: CtaSectionContent | null
+}
+
+export const CtaSection: React.FC = ({ content }) => {
+ const { trigger } = useWebHaptics()
+ if (!content) return
+
+ const staggerContainer: Variants = {
+ hidden: {},
+ show: {
+ transition: {
+ staggerChildren: 0.12,
+ delayChildren: 0.06,
+ },
+ },
+ }
+
+ const staggerItem: Variants = {
+ hidden: { opacity: 0, y: 18 },
+ show: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.42,
+ ease: [0.22, 1, 0.36, 1],
+ },
+ },
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {content.heading}
+
+
+ {content?.subheading}
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/sections/DeploymentSection.tsx b/src/components/sections/DeploymentSection.tsx
new file mode 100644
index 0000000..e6ebcdb
--- /dev/null
+++ b/src/components/sections/DeploymentSection.tsx
@@ -0,0 +1,135 @@
+"use client"
+
+import { Section } from "@/components/ui/Section"
+import { LinkButton } from "@/components/ui/LinkButton"
+import { m as motion, type Variants } from "motion/react"
+import Image from "next/image"
+import React from "react"
+
+interface DeploymentSectionContent {
+ cloudTitle?: string | null
+ cloudDescription?: string | null
+ cloudLink?: {
+ label?: string | null
+ url?: string | null
+ }
+ selfhostTitle?: string | null
+ selfhostDescription?: string | null
+ selfhostLink?: {
+ label?: string | null
+ url?: string | null
+ }
+ dynamicTitle?: string | null
+ dynamicDescription?: string | null
+ dynamicLink?: {
+ label?: string | null
+ url?: string | null
+ }
+}
+
+interface DeploymentSectionProps {
+ content?: DeploymentSectionContent | null
+}
+
+export const DeploymentSection: React.FC = ({ content }) => {
+ if (!content) return null
+
+ const staggerContainer: Variants = {
+ hidden: {},
+ show: {
+ transition: {
+ staggerChildren: 0.12,
+ delayChildren: 0.08,
+ },
+ },
+ }
+
+ const staggerItem: Variants = {
+ hidden: { opacity: 0, y: 20 },
+ show: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.42,
+ ease: [0.22, 1, 0.36, 1],
+ },
+ },
+ }
+
+ const deploymentCards = [
+ {
+ badge: "Cloud",
+ alt: "Cloud deployment",
+ title: content.cloudTitle,
+ description: content.cloudDescription,
+ link: content.cloudLink,
+ glowClass: "from-aqua/24 via-blue/10 to-primary/70",
+ badgeClass: "border-aqua/25 bg-aqua/12 text-aqua",
+ },
+ {
+ badge: "Self-hosted",
+ alt: "Self-hosted deployment",
+ title: content.selfhostTitle,
+ description: content.selfhostDescription,
+ link: content.selfhostLink,
+ glowClass: "from-pink/20 via-blue/10 to-primary/70",
+ badgeClass: "border-pink/25 bg-pink/12 text-pink",
+ },
+ {
+ badge: "Dynamic",
+ alt: "Dynamic deployment",
+ title: content.dynamicTitle,
+ description: content.dynamicDescription,
+ link: content.dynamicLink,
+ glowClass: "from-brand/24 via-aqua/10 to-primary/70",
+ badgeClass: "border-brand/25 bg-brand/12 text-brand",
+ },
+ ] as const
+
+ return (
+
+
+
+
+ {deploymentCards.map((card) => (
+
+
+
+
+
{card.title}
+
{card.description}
+ {card.link?.url && (
+
+ {card.link.label}
+
+ )}
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/sections/FaqSection.tsx b/src/components/sections/FaqSection.tsx
new file mode 100644
index 0000000..5183207
--- /dev/null
+++ b/src/components/sections/FaqSection.tsx
@@ -0,0 +1,68 @@
+"use client"
+
+import { AccordionItem } from "@/components/ui/Accordion"
+import { Section } from "@/components/ui/Section"
+import { m as motion } from "motion/react"
+import React, { useCallback, useState } from "react"
+import { useWebHaptics } from "web-haptics/react"
+
+interface FaqItem {
+ question: string
+ answer: string
+ id?: string | null
+}
+
+interface FaqSectionContent {
+ items: FaqItem[] | null
+}
+
+interface FaqSectionProps {
+ content?: FaqSectionContent | null
+}
+
+export const FaqSection: React.FC = ({ content }) => {
+ const [openItems, setOpenItems] = useState>(new Set())
+ const { trigger } =useWebHaptics()
+
+ const toggleItem = useCallback((index: number) => {
+ trigger("soft")
+ setOpenItems((prevOpenItems) => {
+ const nextOpenItems = new Set(prevOpenItems)
+
+ if (nextOpenItems.has(index)) nextOpenItems.delete(index)
+ else nextOpenItems.add(index)
+
+ return nextOpenItems
+ })
+ }, [trigger])
+
+ if (!content || !content.items) return
+
+ return (
+
+
+
+ {content.items.map((faq, index) => (
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/sections/FooterSection.tsx b/src/components/sections/FooterSection.tsx
new file mode 100644
index 0000000..27a0f00
--- /dev/null
+++ b/src/components/sections/FooterSection.tsx
@@ -0,0 +1,72 @@
+"use client"
+
+import { LandingContainer } from "@/components/ui/LandingContainer"
+import { localizeHref, type AppLocale } from "@/lib/i18n"
+import { DEFAULT_DISCORD_URL, DEFAULT_GITHUB_URL, DEFAULT_INSTAGRAM_URL, DEFAULT_X_URL } from "@/lib/siteConfig"
+import type { Footer } from "@/payload-types"
+import { SiDiscord, SiGithub, SiInstagram, SiX } from "@icons-pack/react-simple-icons"
+import Image from "next/image"
+import Link from "next/link"
+import React from "react"
+import { useWebHaptics } from "web-haptics/react"
+
+interface FooterSectionProps {
+ locale: AppLocale
+ footer: Footer | null
+}
+
+export const FooterSection: React.FC = ({ locale, footer }) => {
+ const { trigger } = useWebHaptics()
+ if (!footer?.groups) return null
+
+ return (
+
+
+
+
+
+
+
+
+ {footer.company_name}
+
+
+
+ trigger("medium")} className="group">
+
+
+ trigger("medium")} className="group">
+
+
+ trigger("medium")} className="group">
+
+
+ trigger("medium")} className="group">
+
+
+
+
+
+ {footer.groups.map((group) => (
+
+
+ {group.heading}
+
+ {(group.items ?? []).map((item) => (
+
trigger("medium")}
+ >
+
+ {item.label}
+
+
+ ))}
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/components/sections/HeroSection.tsx b/src/components/sections/HeroSection.tsx
new file mode 100644
index 0000000..2abb8c4
--- /dev/null
+++ b/src/components/sections/HeroSection.tsx
@@ -0,0 +1,155 @@
+"use client"
+
+import { Section } from "@/components/ui/Section"
+import { cn } from "@/lib/utils"
+import { Button } from "@code0-tech/pictor"
+import { m as motion, type Variants } from "motion/react"
+import Image from "next/image"
+import Link from "next/link"
+import React from "react"
+import Grainient from "../ui/Granient"
+import { useWebHaptics } from "web-haptics/react"
+import { HeroBadge } from "../badges/HeroBadge"
+
+interface HeroSectionButton {
+ label: string
+ url: string
+ variant?: "none" | "normal" | "outlined" | "filled" | null
+ id?: string | null
+}
+
+interface HeroSectionText {
+ text: string
+ id?: string | null
+}
+
+interface HeroSectionContent {
+ badge?: string | null
+ heading?: string | null
+ texts?: HeroSectionText[] | null
+ buttons?: HeroSectionButton[] | null
+}
+
+interface HeroSectionProps {
+ content?: HeroSectionContent | null
+}
+
+export const HeroSection: React.FC = ({ content }) => {
+ const { trigger } = useWebHaptics()
+ if (!content || !content.texts || !content.buttons) return
+
+ const staggerContainer: Variants = {
+ hidden: {},
+ show: {
+ transition: {
+ staggerChildren: 0.12,
+ delayChildren: 0.08,
+ },
+ },
+ }
+
+ const staggerItem: Variants = {
+ hidden: { opacity: 0, y: 18 },
+ show: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.45, ease: [0.22, 1, 0.36, 1] },
+ },
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {content.heading}
+
+
+
+ {content.texts.length > 0
+ ? content.texts.map((item, index) => (
+
+ {item.text}
+ {index < content.texts!!.length - 1 &&
}
+
+ ))
+ : <>Beschreibung1
Beschreibung2>}
+
+
+
+ {content.buttons.map((button, index) => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/sections/RoadmapSection.tsx b/src/components/sections/RoadmapSection.tsx
new file mode 100644
index 0000000..2a58b86
--- /dev/null
+++ b/src/components/sections/RoadmapSection.tsx
@@ -0,0 +1,58 @@
+import { Section } from "@/components/ui/Section"
+import { getRoadmapItems } from "@/lib/cms"
+import { AppLocale } from "@/lib/i18n"
+import React from "react"
+import { RoadmapRevealItem } from "../RoadmapRevealItem"
+
+interface RoadmapSectionProps {
+ locale: AppLocale
+}
+
+export const RoadmapSection: React.FC = async ({ locale }) => {
+ const items = await getRoadmapItems(locale)
+ if (!items?.length) return null
+
+ return (
+
+
+
+
+
+
+ {items.map((item, index) => {
+ const isEven = index % 2 === 0
+
+ return (
+
+
+
+
+
+
*:first-child]:order-2 md:[&>*:last-child]:order-1"}`}>
+
+
+
+
+
+
+
+ {item.time}
+
+
+
{item.title}
+
{item.description}
+
+
+
+
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/src/components/sections/RuntimeFeatureSection.tsx b/src/components/sections/RuntimeFeatureSection.tsx
new file mode 100644
index 0000000..b4e9e3f
--- /dev/null
+++ b/src/components/sections/RuntimeFeatureSection.tsx
@@ -0,0 +1,25 @@
+import { Section } from "@/components/ui/Section"
+import { type AppLocale } from "@/lib/i18n"
+import React from "react"
+import { ActionListCard } from "../cards/ActionListCard"
+import { NodeCard } from "../cards/NodeCard"
+import { RuntimeTypesCard } from "../cards/RuntimeTypesCard"
+import { SuggestionMenuCard } from "../cards/SuggestionMenuCard"
+import { BentoGrid } from "../ui/BentoGrid"
+
+interface RuntimeFeatureSectionProps {
+ locale: AppLocale
+}
+
+export const RuntimeFeatureSection: React.FC = ({ locale }) => {
+ return (
+
+ )
+}
diff --git a/src/components/sections/UseCaseSection.tsx b/src/components/sections/UseCaseSection.tsx
new file mode 100644
index 0000000..ef966e6
--- /dev/null
+++ b/src/components/sections/UseCaseSection.tsx
@@ -0,0 +1,157 @@
+"use client"
+
+import { Section } from "@/components/ui/Section"
+import { ANIMATION_PRESETS, type AnimationPreset } from "@/lib/utils"
+import { m as motion, type Variants } from "motion/react"
+import React from "react"
+
+interface UseCaseItem {
+ label: string
+ title: string
+ description: string
+ bulletPoints: string[]
+ actions: string[]
+ id?: string | null
+}
+
+interface UseCaseSectionContent {
+ useCases: UseCaseItem[] | null
+}
+
+interface UseCaseSectionProps {
+ content?: UseCaseSectionContent | null
+}
+
+const USE_CASE_ANIMATION_SEQUENCE: Exclude[] = [
+ "slide-left",
+ "slide-right",
+ "slide-left"
+]
+
+export const UseCaseSection: React.FC = ({ content }) => {
+ if (!content?.useCases?.length) return null
+
+ const staggerContainer: Variants = {
+ hidden: {},
+ show: {
+ transition: {
+ staggerChildren: 0.08,
+ delayChildren: 0.06,
+ },
+ },
+ }
+
+ const staggerItem: Variants = {
+ hidden: { opacity: 0, y: 14 },
+ show: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.38,
+ ease: [0.22, 1, 0.36, 1],
+ },
+ },
+ }
+
+ return (
+
+
+ {content.useCases.map((item, index) => {
+ const animationPreset = USE_CASE_ANIMATION_SEQUENCE[index % USE_CASE_ANIMATION_SEQUENCE.length]
+ const animationConfig = ANIMATION_PRESETS[animationPreset]
+
+ return (
+
+
+ {item.title}
+ {item.description}
+ {item.bulletPoints?.length ? (
+
+ {item.bulletPoints.map((point, pointIndex) => (
+
+
+ {point}
+
+ ))}
+
+ ) : null}
+ {item.actions?.length ? (
+
+ {item.actions.map((action, actionIndex) => (
+
+ {action}
+
+ ))}
+
+ ) : null}
+
+
+
+
+
+
+
+ {/* Flows darstellen */}
+
+
+
+ {item.title}
+ {item.description}
+ {item.bulletPoints?.length ? (
+
+ {item.bulletPoints.map((point, pointIndex) => (
+
+
+ {point}
+
+ ))}
+
+ ) : null}
+ {item.actions?.length ? (
+
+ {item.actions.map((action, actionIndex) => (
+
+ {action}
+
+ ))}
+
+ ) : null}
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/src/components/tables/OrganizationsDataTable.tsx b/src/components/tables/OrganizationsDataTable.tsx
new file mode 100644
index 0000000..a48d316
--- /dev/null
+++ b/src/components/tables/OrganizationsDataTable.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import { Avatar, Card, Text } from "@code0-tech/pictor"
+
+export function OrganizationsDataTable() {
+ const organizations = ["Cygnus Labs", "Atlas Systems", "Nova Ops", "Orion Collective", "Pulse Ventures"]
+
+ return (
+
+ Organizations
+
+ {organizations.map((organization) => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/tables/ProjectDataTable.tsx b/src/components/tables/ProjectDataTable.tsx
new file mode 100644
index 0000000..33d4b47
--- /dev/null
+++ b/src/components/tables/ProjectDataTable.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import { Avatar, Card, Text } from "@code0-tech/pictor"
+
+export function ProjectDataTable() {
+ const projects = [
+ { name: "Test" },
+ { name: "Test2" },
+ { name: "Test3" }
+ ]
+
+ return (
+
+ Personal Projects
+
+ {projects.map((project) => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/ui/Accordion.tsx b/src/components/ui/Accordion.tsx
new file mode 100644
index 0000000..d85ccea
--- /dev/null
+++ b/src/components/ui/Accordion.tsx
@@ -0,0 +1,90 @@
+import {IconChevronDown} from "@tabler/icons-react"
+import { m as motion } from "motion/react"
+import React from "react"
+import { cn } from "@/lib/utils"
+
+const accordionCardBaseClassName =
+ "group relative z-10 w-full cursor-pointer overflow-hidden rounded-2xl hover:bg-white/5 transition-colors border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] shadow-[0_18px_50px_rgba(0,0,0,0.22)] before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-linear-to-r before:from-transparent before:via-white/30 before:to-transparent before:content-['']"
+
+const accordionCardOpenClassName =
+ "border-white/16 bg-[linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0.03))] shadow-[0_24px_70px_rgba(0,0,0,0.3)]"
+
+interface FAQItemProps {
+ index: number
+ question: string
+ answer: string
+ isOpen: boolean
+ onToggle: (index: number) => void
+}
+
+const AccordionItemComponent = ({ index, question, answer, isOpen, onToggle }: FAQItemProps) => {
+ const handleClick = (e: React.MouseEvent) => {
+ e.preventDefault()
+ onToggle(index)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export const AccordionItem = React.memo(AccordionItemComponent)
diff --git a/src/components/ui/Aurora.tsx b/src/components/ui/Aurora.tsx
new file mode 100644
index 0000000..92b0faa
--- /dev/null
+++ b/src/components/ui/Aurora.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import { AuroraBackground } from "@code0-tech/pictor"
+
+export function Aurora() {
+ return
+}
diff --git a/src/components/ui/BentoGrid.tsx b/src/components/ui/BentoGrid.tsx
new file mode 100644
index 0000000..653bdf7
--- /dev/null
+++ b/src/components/ui/BentoGrid.tsx
@@ -0,0 +1,23 @@
+import { ReactNode } from "react"
+
+interface BentoGridProps {
+ children: ReactNode
+ columns?: number
+}
+
+export function BentoGrid({ children, columns = 5 }: BentoGridProps) {
+ const colClass = {
+ 1: "md:grid-cols-1",
+ 2: "md:grid-cols-2",
+ 3: "md:grid-cols-3",
+ 4: "md:grid-cols-4",
+ 5: "md:grid-cols-5",
+ 6: "md:grid-cols-6",
+ }[columns] ?? "md:grid-cols-5"
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/ui/Granient.tsx b/src/components/ui/Granient.tsx
new file mode 100644
index 0000000..74d59a9
--- /dev/null
+++ b/src/components/ui/Granient.tsx
@@ -0,0 +1,266 @@
+import React, { useEffect, useRef } from 'react';
+import { Renderer, Program, Mesh, Triangle } from 'ogl';
+
+interface GrainientProps {
+ timeSpeed?: number;
+ colorBalance?: number;
+ warpStrength?: number;
+ warpFrequency?: number;
+ warpSpeed?: number;
+ warpAmplitude?: number;
+ blendAngle?: number;
+ blendSoftness?: number;
+ rotationAmount?: number;
+ noiseScale?: number;
+ grainAmount?: number;
+ grainScale?: number;
+ grainAnimated?: boolean;
+ contrast?: number;
+ gamma?: number;
+ saturation?: number;
+ centerX?: number;
+ centerY?: number;
+ zoom?: number;
+ color1?: string;
+ color2?: string;
+ color3?: string;
+ className?: string;
+}
+
+const hexToRgb = (hex: string): [number, number, number] => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ if (!result) return [1, 1, 1];
+ return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];
+};
+
+const vertex = `#version 300 es
+in vec2 position;
+void main() {
+ gl_Position = vec4(position, 0.0, 1.0);
+}
+`;
+
+const fragment = `#version 300 es
+precision highp float;
+uniform vec2 iResolution;
+uniform float iTime;
+uniform float uTimeSpeed;
+uniform float uColorBalance;
+uniform float uWarpStrength;
+uniform float uWarpFrequency;
+uniform float uWarpSpeed;
+uniform float uWarpAmplitude;
+uniform float uBlendAngle;
+uniform float uBlendSoftness;
+uniform float uRotationAmount;
+uniform float uNoiseScale;
+uniform float uGrainAmount;
+uniform float uGrainScale;
+uniform float uGrainAnimated;
+uniform float uContrast;
+uniform float uGamma;
+uniform float uSaturation;
+uniform vec2 uCenterOffset;
+uniform float uZoom;
+uniform vec3 uColor1;
+uniform vec3 uColor2;
+uniform vec3 uColor3;
+out vec4 fragColor;
+#define S(a,b,t) smoothstep(a,b,t)
+mat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);}
+vec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);}
+float noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}
+void mainImage(out vec4 o, vec2 C){
+ float t=iTime*uTimeSpeed;
+ vec2 uv=C/iResolution.xy;
+ float ratio=iResolution.x/iResolution.y;
+ vec2 tuv=uv-0.5+uCenterOffset;
+ tuv/=max(uZoom,0.001);
+
+ float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);
+ tuv.y*=1.0/ratio;
+ tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));
+ tuv.y*=ratio;
+
+ float frequency=uWarpFrequency;
+ float ws=max(uWarpStrength,0.001);
+ float amplitude=uWarpAmplitude/ws;
+ float warpTime=t*uWarpSpeed;
+ tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;
+ tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);
+
+ vec3 colLav=uColor1;
+ vec3 colOrg=uColor2;
+ vec3 colDark=uColor3;
+ float b=uColorBalance;
+ float s=max(uBlendSoftness,0.0);
+ mat2 blendRot=Rot(radians(uBlendAngle));
+ float blendX=(tuv*blendRot).x;
+ float edge0=-0.3-b-s;
+ float edge1=0.2-b+s;
+ float v0=0.5-b+s;
+ float v1=-0.3-b-s;
+ vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));
+ vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));
+ vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));
+
+ vec2 grainUv=uv*max(uGrainScale,0.001);
+ if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);}
+ float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);
+ col+=(grain-0.5)*uGrainAmount;
+
+ col=(col-0.5)*uContrast+0.5;
+ float luma=dot(col,vec3(0.2126,0.7152,0.0722));
+ col=mix(vec3(luma),col,uSaturation);
+ col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));
+ col=clamp(col,0.0,1.0);
+
+ o=vec4(col,1.0);
+}
+void main(){
+ vec4 o=vec4(0.0);
+ mainImage(o,gl_FragCoord.xy);
+ fragColor=o;
+}
+`;
+
+const Grainient: React.FC = ({
+ timeSpeed = 0.25,
+ colorBalance = 0.0,
+ warpStrength = 1.0,
+ warpFrequency = 5.0,
+ warpSpeed = 2.0,
+ warpAmplitude = 50.0,
+ blendAngle = 0.0,
+ blendSoftness = 0.05,
+ rotationAmount = 500.0,
+ noiseScale = 2.0,
+ grainAmount = 0.1,
+ grainScale = 2.0,
+ grainAnimated = false,
+ contrast = 1.5,
+ gamma = 1.0,
+ saturation = 1.0,
+ centerX = 0.0,
+ centerY = 0.0,
+ zoom = 0.9,
+ color1 = '#FF9FFC',
+ color2 = '#5227FF',
+ color3 = '#B19EEF',
+ className = ''
+}) => {
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ if (!containerRef.current) return;
+
+ const renderer = new Renderer({
+ webgl: 2,
+ alpha: true,
+ antialias: false,
+ dpr: Math.min(window.devicePixelRatio || 1, 2)
+ });
+
+ const gl = renderer.gl;
+ const canvas = gl.canvas as HTMLCanvasElement;
+ canvas.style.width = '100%';
+ canvas.style.height = '100%';
+ canvas.style.display = 'block';
+
+ const container = containerRef.current;
+ container.appendChild(canvas);
+
+ const geometry = new Triangle(gl);
+ const program = new Program(gl, {
+ vertex,
+ fragment,
+ uniforms: {
+ iTime: { value: 0 },
+ iResolution: { value: new Float32Array([1, 1]) },
+ uTimeSpeed: { value: timeSpeed },
+ uColorBalance: { value: colorBalance },
+ uWarpStrength: { value: warpStrength },
+ uWarpFrequency: { value: warpFrequency },
+ uWarpSpeed: { value: warpSpeed },
+ uWarpAmplitude: { value: warpAmplitude },
+ uBlendAngle: { value: blendAngle },
+ uBlendSoftness: { value: blendSoftness },
+ uRotationAmount: { value: rotationAmount },
+ uNoiseScale: { value: noiseScale },
+ uGrainAmount: { value: grainAmount },
+ uGrainScale: { value: grainScale },
+ uGrainAnimated: { value: grainAnimated ? 1.0 : 0.0 },
+ uContrast: { value: contrast },
+ uGamma: { value: gamma },
+ uSaturation: { value: saturation },
+ uCenterOffset: { value: new Float32Array([centerX, centerY]) },
+ uZoom: { value: zoom },
+ uColor1: { value: new Float32Array(hexToRgb(color1)) },
+ uColor2: { value: new Float32Array(hexToRgb(color2)) },
+ uColor3: { value: new Float32Array(hexToRgb(color3)) }
+ }
+ });
+
+ const mesh = new Mesh(gl, { geometry, program });
+
+ const setSize = () => {
+ const rect = container.getBoundingClientRect();
+ const width = Math.max(1, Math.floor(rect.width));
+ const height = Math.max(1, Math.floor(rect.height));
+ renderer.setSize(width, height);
+ const res = (program.uniforms.iResolution as { value: Float32Array }).value;
+ res[0] = gl.drawingBufferWidth;
+ res[1] = gl.drawingBufferHeight;
+ };
+
+ const ro = new ResizeObserver(setSize);
+ ro.observe(container);
+ setSize();
+
+ let raf = 0;
+ const t0 = performance.now();
+ const loop = (t: number) => {
+ (program.uniforms.iTime as { value: number }).value = (t - t0) * 0.001;
+ renderer.render({ scene: mesh });
+ raf = requestAnimationFrame(loop);
+ };
+ raf = requestAnimationFrame(loop);
+
+ return () => {
+ cancelAnimationFrame(raf);
+ ro.disconnect();
+ try {
+ container.removeChild(canvas);
+ } catch {
+ // Ignore
+ }
+ };
+ }, [
+ timeSpeed,
+ colorBalance,
+ warpStrength,
+ warpFrequency,
+ warpSpeed,
+ warpAmplitude,
+ blendAngle,
+ blendSoftness,
+ rotationAmount,
+ noiseScale,
+ grainAmount,
+ grainScale,
+ grainAnimated,
+ contrast,
+ gamma,
+ saturation,
+ centerX,
+ centerY,
+ zoom,
+ color1,
+ color2,
+ color3
+ ]);
+
+ return ;
+};
+
+export default Grainient;
diff --git a/src/components/ui/LandingContainer.tsx b/src/components/ui/LandingContainer.tsx
new file mode 100644
index 0000000..204abc2
--- /dev/null
+++ b/src/components/ui/LandingContainer.tsx
@@ -0,0 +1,13 @@
+"use client"
+
+import {Container} from "@code0-tech/pictor"
+import React from "react"
+import {cn} from "@/lib/utils"
+
+export function LandingContainer({ children, className }: { children: React.ReactNode, className?: string }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/ui/LinkButton.tsx b/src/components/ui/LinkButton.tsx
new file mode 100644
index 0000000..4a880cb
--- /dev/null
+++ b/src/components/ui/LinkButton.tsx
@@ -0,0 +1,36 @@
+"use client"
+
+import * as React from "react"
+import { IconArrowUpRight } from "@tabler/icons-react"
+import { cn } from "@/lib/utils"
+import Link, { LinkProps } from "next/link"
+import { useWebHaptics } from "web-haptics/react"
+
+interface LinkButtonProps extends LinkProps {
+ href: string
+ children: React.ReactNode
+ showArrow?: boolean
+ className?: string
+}
+
+const baseClassName =
+ "w-max h-auto min-w-0 px-0 py-0 text-sm inline-flex items-center justify-center gap-1 border-b border-dashed border-white/25" +
+ "rounded-none cursor-pointer text-gray-500 hover:text-brand hover:border-brand transition-colors disabled:opacity-50" +
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30 disabled:pointer-events-none"
+
+
+export function LinkButton({ className, children, href, showArrow = true, ...props }: LinkButtonProps) {
+ const { trigger } = useWebHaptics()
+
+ return (
+ trigger("medium")}
+ className={cn(baseClassName, className)}
+ {...props}
+ >
+ {children}
+ {showArrow && }
+
+ )
+}
diff --git a/src/components/ui/Section.tsx b/src/components/ui/Section.tsx
new file mode 100644
index 0000000..9c8dca4
--- /dev/null
+++ b/src/components/ui/Section.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import { usePreloadedSection } from "@/components/providers/SectionsProvider"
+import { LinkButton } from "@/components/ui/LinkButton"
+import { getLocaleFromPath, localizeHref } from "@/lib/i18n"
+import { ANIMATION_PRESETS, cn, type AnimationPreset } from "@/lib/utils"
+import { Section as SectionDocument } from "@/payload-types"
+import { m as motion, type Variants } from "motion/react"
+import { usePathname } from "next/navigation"
+import { ReactNode } from "react"
+
+interface SectionProps {
+ children: ReactNode
+ funnelType?: "center" | "left"
+ className?: string
+ sectionType?: NonNullable
+ showBlur?: boolean
+ showFunnel?: boolean
+ showLinkButton?: boolean
+ fullHeight?: boolean
+ animationPreset?: AnimationPreset
+ animationDelay?: number
+ animationDuration?: number
+ animationOnce?: boolean
+}
+
+export function Section({
+ sectionType,
+ children,
+ className,
+ funnelType = "center",
+ showBlur = true,
+ showFunnel = true,
+ showLinkButton = true,
+ fullHeight = false,
+ animationPreset = "fade-up",
+ animationDelay = 0,
+ animationDuration,
+ animationOnce = true,
+}: SectionProps) {
+ const sectionData = usePreloadedSection(sectionType) as SectionDocument | null
+ const pathname = usePathname()
+ const locale = getLocaleFromPath(pathname)
+ const rawLinkUrl = sectionData?.link_button?.url?.trim()
+ const linkUrl = rawLinkUrl ? localizeHref(rawLinkUrl, locale) : undefined
+ const animationConfig = animationPreset === "none" ? null : ANIMATION_PRESETS[animationPreset]
+ const staggerContainer: Variants = {
+ hidden: {},
+ show: {
+ transition: {
+ staggerChildren: 0.1,
+ delayChildren: 0.04,
+ },
+ },
+ }
+ const staggerItem: Variants = {
+ hidden: { opacity: 0, y: 16 },
+ show: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.4,
+ ease: [0.22, 1, 0.36, 1],
+ },
+ },
+ }
+
+ return (
+
+ {showBlur && (funnelType === "center" ? (
+
+ ) : (
+
+ ))}
+ {showFunnel && (
+ funnelType === "center" ? (
+
+
+ {sectionData?.heading}
+
+
+ {sectionData?.subheading}
+
+ {showLinkButton && linkUrl &&
+
+
+ {sectionData?.link_button?.label}
+
+
+ }
+
+ ) : (
+
+
+ {sectionData?.heading}
+
+
+ {sectionData?.subheading}
+
+ {showLinkButton && linkUrl &&
+
+
+ {sectionData?.link_button?.label}
+
+
+ }
+
+ )
+ )}
+ {children}
+
+ )
+}
diff --git a/src/components/ui/SuggesstionMenuClient.tsx b/src/components/ui/SuggesstionMenuClient.tsx
new file mode 100644
index 0000000..4afd7bd
--- /dev/null
+++ b/src/components/ui/SuggesstionMenuClient.tsx
@@ -0,0 +1,234 @@
+"use client"
+
+import type { ReactNode } from "react"
+import type { InputSuggestion } from "@code0-tech/pictor"
+import { Card, Text } from "@code0-tech/pictor"
+import { IconBulb, IconChevronUp, IconCircleDot, IconCirclesRelation, IconFileFunctionFilled } from "@tabler/icons-react"
+
+const FunctionSuggestionType = {
+ FUNCTION: "FUNCTION",
+ FUNCTION_COMBINATION: "FUNCTION_COMBINATION",
+ REF_OBJECT: "REF_OBJECT",
+ VALUE: "VALUE",
+ DATA_TYPE: "DATA_TYPE",
+} as const
+
+type FunctionSuggestionType = typeof FunctionSuggestionType[keyof typeof FunctionSuggestionType]
+
+type SuggestionWithType = InputSuggestion & {
+ suggestionType: FunctionSuggestionType
+}
+
+export function SuggesstionMenuClient() {
+ const iconMap: Record = {
+ [FunctionSuggestionType.FUNCTION]: ,
+ [FunctionSuggestionType.FUNCTION_COMBINATION]: ,
+ [FunctionSuggestionType.REF_OBJECT]: ,
+ [FunctionSuggestionType.VALUE]: ,
+ [FunctionSuggestionType.DATA_TYPE]: ,
+ }
+
+ const suggestions: SuggestionWithType[] = [
+ {
+ children: "0-0-2-",
+ value: "0-0-2-",
+ valueData: { id: "variable_1", type: "variable", label: "0-0-2-" },
+ groupBy: "Variables",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.REF_OBJECT,
+ },
+ {
+ children: "Boolean from Number",
+ value: "boolean-from-number",
+ valueData: { id: "std_boolean_1", type: "action", label: "Boolean from Number" },
+ groupBy: "STD::BOOLEAN",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Boolean from Text",
+ value: "boolean-from-text",
+ valueData: { id: "std_boolean_2", type: "action", label: "Boolean from Text" },
+ groupBy: "STD::BOOLEAN",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Is Equal",
+ value: "is-equal-boolean",
+ valueData: { id: "std_boolean_3", type: "action", label: "Is Equal" },
+ groupBy: "STD::BOOLEAN",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Negate Boolean",
+ value: "negate-boolean",
+ valueData: { id: "std_boolean_4", type: "action", label: "Negate Boolean" },
+ groupBy: "STD::BOOLEAN",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Get Element of List",
+ value: "get-element-of-list",
+ valueData: { id: "std_list_1", type: "action", label: "Get Element of List" },
+ groupBy: "STD::LIST",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Find Element in List",
+ value: "find-element-in-list",
+ valueData: { id: "std_list_2", type: "action", label: "Find Element in List" },
+ groupBy: "STD::LIST",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Find Last Element in List",
+ value: "find-last-element-in-list",
+ valueData: { id: "std_list_3", type: "action", label: "Find Last Element in List" },
+ groupBy: "STD::LIST",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "First Element of List",
+ value: "first-element-of-list",
+ valueData: { id: "std_list_4", type: "action", label: "First Element of List" },
+ groupBy: "STD::LIST",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Is List Empty",
+ value: "is-list-empty",
+ valueData: { id: "std_list_5", type: "action", label: "Is List Empty" },
+ groupBy: "STD::LIST",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Last Element of List",
+ value: "last-element-of-list",
+ valueData: { id: "std_list_6", type: "action", label: "Last Element of List" },
+ groupBy: "STD::LIST",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Pop from List",
+ value: "pop-from-list",
+ valueData: { id: "std_list_7", type: "action", label: "Pop from List" },
+ groupBy: "STD::LIST",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Is Equal",
+ value: "is-equal-number",
+ valueData: { id: "std_number_1", type: "action", label: "Is Equal" },
+ groupBy: "STD::NUMBER",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Is Greater",
+ value: "is-greater",
+ valueData: { id: "std_number_2", type: "action", label: "Is Greater" },
+ groupBy: "STD::NUMBER",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ {
+ children: "Is Less",
+ value: "is-less",
+ valueData: { id: "std_number_3", type: "action", label: "Is Less" },
+ groupBy: "STD::NUMBER",
+ insertMode: "insert",
+ suggestionType: FunctionSuggestionType.FUNCTION,
+ },
+ ]
+
+ const groupedSuggestions = suggestions.reduce>((acc, suggestion) => {
+ const group = suggestion.groupBy || "Suggestions"
+
+ if (!acc[group]) {
+ acc[group] = []
+ }
+
+ acc[group].push(suggestion)
+ return acc
+ }, {})
+ const groupedEntries = Object.entries(groupedSuggestions)
+
+ return (
+
+
+ {groupedEntries
+ .filter(([group]) => group === "Variables")
+ .map(([group, items]) => (
+
+
+ {group}
+
+
+
+ {items.map((suggestion) => (
+
+
+ {iconMap[suggestion.suggestionType]}
+
+
+ {String(suggestion.children ?? suggestion.value)}
+
+
+ ))}
+
+
+ ))}
+
+
+ {groupedEntries
+ .filter(([group]) => group !== "Variables")
+ .map(([group, items]) => (
+
+
+ {group}
+
+
+
+ {items.map((suggestion) => (
+
+
+ {iconMap[suggestion.suggestionType]}
+
+
+ {String(suggestion.children ?? suggestion.value)}
+
+
+ ))}
+
+
+ ))}
+
+
+
+ Press
+ ↵
+ to insert
+
+
+
+ )
+}
diff --git a/src/hooks/useMediaQuery.ts b/src/hooks/useMediaQuery.ts
new file mode 100644
index 0000000..5a82c96
--- /dev/null
+++ b/src/hooks/useMediaQuery.ts
@@ -0,0 +1,20 @@
+import * as React from "react"
+import { useState } from "react"
+
+export function useMediaQuery(query: string) {
+ const [value, setValue] = useState(false)
+
+ React.useEffect(() => {
+ function onChange(event: MediaQueryListEvent) {
+ setValue(event.matches)
+ }
+
+ const result = matchMedia(query)
+ result.addEventListener("change", onChange)
+ setValue(result.matches)
+
+ return () => result.removeEventListener("change", onChange)
+ }, [query])
+
+ return value
+}
\ No newline at end of file
diff --git a/src/hooks/useOutsideClick.ts b/src/hooks/useOutsideClick.ts
new file mode 100644
index 0000000..3f8baf3
--- /dev/null
+++ b/src/hooks/useOutsideClick.ts
@@ -0,0 +1,27 @@
+import {RefObject, useEffect, useRef} from "react"
+
+type EventHandler = (e: MouseEvent | TouchEvent) => void;
+
+function useOutsideClick(callback: EventHandler): RefObject {
+ const ref = useRef(null)
+
+ useEffect(() => {
+ const handle = (e: MouseEvent | TouchEvent) => {
+ const el = ref.current
+ if (!el || el.contains(e.target as Node)) return
+ callback(e)
+ }
+
+ document.addEventListener("mousedown", handle)
+ document.addEventListener("touchstart", handle)
+
+ return () => {
+ document.removeEventListener("mousedown", handle)
+ document.removeEventListener("touchstart", handle)
+ }
+ }, [callback])
+
+ return ref
+}
+
+export {useOutsideClick}
diff --git a/src/lib/cms.ts b/src/lib/cms.ts
new file mode 100644
index 0000000..81a0e50
--- /dev/null
+++ b/src/lib/cms.ts
@@ -0,0 +1,378 @@
+"use server"
+
+import type { Blog, Feature, Footer, Job, Media, NavbarItem, Page, RoadmapItem as PayloadRoadmapItem, Section, User } from "@/payload-types"
+import { DEFAULT_LOCALE, type AppLocale } from "@/lib/i18n"
+import { getPayloadClient } from "@/lib/payloadClient"
+import { unstable_cache } from "next/cache"
+
+type PageLayoutBlock = NonNullable[number]
+
+export type HeroLayoutBlock = Extract
+export type BrandLayoutBlock = Extract
+export type CtaLayoutBlock = Extract
+export type FaqLayoutBlock = Extract
+export type UseCaseLayoutBlock = Extract
+export type DeploymentLayoutBlock = Extract
+export type JobsLayoutBlock = Extract
+export type MarkdownLayoutBlock = Extract
+export type ContactLayoutBlock = Extract
+
+type FeatureSlug = Feature["slug"]
+interface FeatureItem {
+ id: Feature["id"]
+ slug: FeatureSlug
+ title: NonNullable
+ description: NonNullable
+ link: {
+ label: NonNullable["label"]>
+ url: NonNullable["url"]>
+ }
+}
+
+export type JobItem = Pick
+type JobDetailItem = Pick
+export type TeamMemberItem = Pick
+type RoadmapItem = Pick
+
+export type BlogPostItem = Pick & {
+ heroImage?: (number | null) | Media
+ author: number | Pick
+}
+
+const getLandingPageCached = unstable_cache(
+ async (cachedSlug: string, cachedLocale: AppLocale): Promise => {
+ const payload = await getPayloadClient()
+
+ const result = await payload.find({
+ collection: "pages",
+ locale: cachedLocale,
+ fallbackLocale: DEFAULT_LOCALE,
+ where: { slug: { equals: cachedSlug } },
+ limit: 1,
+ depth: 1,
+ pagination: false,
+ })
+
+ return (result.docs[0] as Page | undefined) ?? null
+ },
+ ["landing-page"],
+ { revalidate: 300, tags: ["pages"] },
+)
+
+const getNavbarItemsCached = unstable_cache(
+ async (locale: AppLocale): Promise => {
+ const payload = await getPayloadClient()
+
+ const result = await payload.find({
+ collection: "navbarItems",
+ locale,
+ fallbackLocale: DEFAULT_LOCALE,
+ pagination: false,
+ sort: "order",
+ depth: 0,
+ })
+
+ return result.docs as NavbarItem[]
+ },
+ ["navbar-items"],
+ { revalidate: 300, tags: ["navbarItems"] },
+)
+
+const getFooterCached = unstable_cache(
+ async (locale: AppLocale): Promise