Initial commit

This commit is contained in:
Francesco
2025-05-14 14:35:15 +02:00
commit c482b6b254
130 changed files with 13171 additions and 0 deletions

70
app/actions/contact.ts Normal file
View File

@@ -0,0 +1,70 @@
"use server"
import { z } from "zod"
// Define a schema for form validation
const ContactFormSchema = z.object({
firstName: z.string().min(1, "Il nome è obbligatorio"),
lastName: z.string().min(1, "Il cognome è obbligatorio"),
email: z.string().email("Indirizzo email non valido"),
company: z.string().optional(),
message: z.string().min(10, "Il messaggio deve contenere almeno 10 caratteri"),
})
type ContactFormData = z.infer<typeof ContactFormSchema>
export async function submitContactForm(formData: FormData) {
// Artificial delay to simulate network request
await new Promise((resolve) => setTimeout(resolve, 1000))
try {
// Extract and validate form data
const data = {
firstName: formData.get("firstName") as string,
lastName: formData.get("lastName") as string,
email: formData.get("email") as string,
company: formData.get("company") as string,
message: formData.get("message") as string,
} as ContactFormData
// Validate the form data
const validatedData = ContactFormSchema.parse(data)
// In a real application, you would send an email here
// For example, using a service like SendGrid, Mailgun, etc.
console.log("Form submission:", validatedData)
// For demonstration purposes, let's log what would happen in a real app
console.log(`
In a production environment, an email would be sent with:
To: contact@techtonicfault.com
From: ${validatedData.email}
Subject: New contact form submission from ${validatedData.firstName} ${validatedData.lastName}
Body: ${validatedData.message}
Additional Info: Company - ${validatedData.company || "Not provided"}
`)
// Return success response
return {
success: true,
message: "Grazie per l'interesse! Ti ricontatteremo a breve.",
}
} catch (error) {
console.error("Form validation error:", error)
if (error instanceof z.ZodError) {
// Return validation errors
const errorMessages = error.errors.map((err) => `${err.message}`).join(", ")
return {
success: false,
message: `Ricontrolla i dati: ${errorMessages}`,
}
}
// Return generic error
return {
success: false,
message: "Qualcosa è andato storto. Riprova più tardi.",
}
}
}

View File

@@ -0,0 +1,32 @@
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Navbar } from "@/components/navbar"
import { Footer } from "@/components/footer"
import { ArrowLeft } from "lucide-react"
export default function BlogPostNotFound() {
return (
<div className="flex min-h-screen flex-col">
<Navbar />
<main className="flex-1 flex items-center justify-center">
<div className="container px-4 md:px-6 py-24 flex flex-col items-center text-center">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl text-primary-900 dark:text-primary-300 mb-4">
Post non trovato
</h1>
<p className="max-w-[600px] text-gray-500 dark:text-gray-400 md:text-xl/relaxed mb-8">
Il post che cercavi potrebbe essere stato eliminato o spostato.
</p>
<Link href="/blog">
<Button className="bg-primary-900 hover:bg-primary-800 dark:bg-primary-700 dark:hover:bg-primary-600">
<ArrowLeft />
Blog
</Button>
</Link>
</div>
</main>
<Footer />
</div>
)
}

158
app/blog/[slug]/page.tsx Normal file
View File

@@ -0,0 +1,158 @@
import { notFound } from "next/navigation"
import { Navbar } from "@/components/navbar"
import { Footer } from "@/components/footer"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { ArrowLeft, Calendar, Clock } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import type { Metadata } from "next"
import { MDXRemote } from "next-mdx-remote/rsc"
import rehypePrism from "rehype-prism-plus"
import blogPosts from "@/data/blog"
interface BlogPostPageProps {
params: {
slug: string
}
}
export async function generateMetadata({ params }: BlogPostPageProps): Promise<Metadata> {
const p = await params;
const post = blogPosts.find((post) => post.slug === p.slug)
if (!post) {
return {
title: "Post non trovato",
}
}
return {
title: `${post.title} | TECHTONIC FAULT Blog`,
description: post.excerpt,
}
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const p = await params;
const post = blogPosts.find((post) => post.slug === p.slug)
if (!post) {
notFound()
}
return (
<div className="flex min-h-screen flex-col">
<Navbar />
<main className="flex-1">
{/* Hero Section */}
<section className="w-full py-12 md:py-24 lg:py-32 bg-primary-50 dark:bg-gray-900">
<div className="container px-4 md:px-6 mx-auto">
<div className="flex flex-col items-center text-center space-y-4 max-w-3xl mx-auto">
<Badge className="bg-primary-100 text-primary-900 border-primary-200 hover:bg-primary-100">
{post.category}
</Badge>
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl text-primary-900 dark:text-primary-300">
{post.title}
</h1>
<div className="flex items-center justify-center gap-4 text-gray-500 dark:text-gray-400">
<div className="flex items-center">
<Calendar className="h-4 w-4 mr-2" />
{post.date}
</div>
<div className="flex items-center">
<Clock className="h-4 w-4 mr-2" />
{post.readTime}
</div>
</div>
<div className="flex items-center justify-center gap-2 mt-4">
<Avatar className="h-10 w-10">
<AvatarImage src={post.author.avatar || "/placeholder.svg"} alt={post.author.name} />
<AvatarFallback>
{post.author.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="text-left">
<p className="text-sm font-medium text-primary-900 dark:text-primary-300">{post.author.name}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{post.author.title}</p>
</div>
</div>
</div>
</div>
</section>
{/* Featured Image */}
<div className="relative w-full h-[300px] md:h-[500px] lg:h-[600px]">
<Image src={post.coverImage || "/placeholder.svg"} alt={post.title} fill className="object-cover" priority />
</div>
{/* Content */}
<article className="container px-4 md:px-6 mx-auto py-12 md:py-24">
<div className="prose prose-purple dark:prose-invert max-w-3xl mx-auto">
<MDXRemote
source={post.content}
options={{
mdxOptions: {
rehypePlugins: [rehypePrism],
},
}}
/>
</div>
<div className="flex flex-wrap gap-2 max-w-3xl mx-auto mt-12">
{post.tags.map((tag, index) => (
<Badge
key={index}
variant="secondary"
className="bg-primary-100 text-primary-900 dark:bg-primary-900 dark:text-primary-100"
>
{tag}
</Badge>
))}
</div>
<div className="max-w-3xl mx-auto mt-12 pt-12 border-t border-gray-200 dark:border-gray-800">
<div className="flex flex-col gap-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-4">
<Avatar className="h-16 w-16">
<AvatarImage src={post.author.avatar || "/placeholder.svg"} alt={post.author.name} />
<AvatarFallback>
{post.author.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div>
<p className="text-lg font-medium text-primary-900 dark:text-primary-300">{post.author.name}</p>
<p className="text-gray-500 dark:text-gray-400">{post.author.title}</p>
</div>
</div>
<Link href="/blog">
<Button
variant="outline"
className="border-primary-200 text-primary-900 dark:border-primary-800 dark:text-primary-300"
>
<ArrowLeft />
Blog
</Button>
</Link>
</div>
<p>
{post.author.description}
</p>
</div>
</div>
</article>
</main>
<Footer />
</div>
)
}

64
app/blog/page.tsx Normal file
View File

@@ -0,0 +1,64 @@
import { Navbar } from "@/components/navbar"
import { Footer } from "@/components/footer"
import { Badge } from "@/components/ui/badge"
import { BlogCard } from "@/components/blog-card"
import type { Metadata } from "next"
import { Animate } from "@/components/animations/animate"
import { Stagger } from "@/components/animations/stagger"
import blogPosts from "@/data/blog"
export const metadata: Metadata = {
title: "Blog | TECHTONIC FAULT",
description: "Insight e articoli su sviluppo software, tecnologia e trasformazione digitale",
}
export default function BlogPage() {
return (
<div className="flex min-h-screen flex-col">
<Navbar />
<main className="flex-1">
{/* Hero Section */}
<section className="w-full py-12 md:py-24 lg:py-32 bg-primary-50 dark:bg-gray-900">
<div className="container px-4 md:px-6 mx-auto">
<div className="flex flex-col items-center text-center space-y-4">
<Animate animation="fadeInUp" delay={0.1}>
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl text-primary-900 dark:text-primary-300">
Ultimi articoli
</h1>
</Animate>
<Animate animation="fadeInUp" delay={0.2}>
<p className="max-w-[900px] text-gray-500 dark:text-gray-400 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
Resta aggiornato con i nostri ultimi post su tecnologia, sviluppo e trend d'industria.
</p>
</Animate>
</div>
</div>
</section>
{/* Blog Posts */}
<section className="w-full py-12 md:py-24 bg-white dark:bg-gray-950">
<div className="container px-4 md:px-6 mx-auto">
<Stagger className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" delay={0.3}>
{blogPosts.map((post) => (
<BlogCard key={post.slug} post={post} />
))}
</Stagger>
</div>
</section>
{!blogPosts.length && <>
<section className="w-full py-12 md:py-24 bg-white dark:bg-gray-950">
<div className="flex flex-col items-center text-center space-y-4 -mt-32 container px-4 md:px-6 mx-auto text-gray-500 dark:text-gray-400">
<Animate delay={0.3}>
Non ci sono post.
</Animate>
</div>
</section>
</>}
</main>
<Footer />
</div>
)
}

180
app/code.css Normal file
View File

@@ -0,0 +1,180 @@
/*
Name: Duotone Sea
Author: by Simurai, adapted from DuoTone themes by Simurai for Atom (http://simurai.com/projects/2016/01/01/duotone-themes)
Conversion: Bram de Haan (http://atelierbram.github.io/Base2Tone-prism/output/prism/prism-base2tone-sea-dark.css)
Generated with Base16 Builder (https://github.com/base16-builder/base16-builder)
*/
code[class*="language-"],
pre[class*="language-"] {
font-family: Consolas, Menlo, Monaco, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", "Courier New", Courier, monospace;
font-size: 14px;
line-height: 1.375;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
/* background: #2a262c; */
color: #775f99;
}
pre > code[class*="language-"] {
font-size: 1em;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #6e00a6;
color: white;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background: #6e00a6;
color: white;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #85679e;
font-style: italic;
}
.token.punctuation {
color: #6c4e84;
}
.token.namespace {
opacity: .7;
}
.token.tag,
.token.operator,
.token.number {
color: #b369dd;
}
.token.property,
.token.function {
color: #856fa5;
}
.token.tag-id,
.token.selector,
.token.atrule-id {
color: #f5edff;
}
code.language-javascript,
.token.attr-name {
color: #c98cff;
}
code.language-css,
code.language-scss,
.token.boolean,
.token.string,
.token.entity,
.token.url,
.language-css .token.string,
.language-scss .token.string,
.style .token.string,
.token.attr-value,
.token.keyword,
.token.control,
.token.directive,
.token.unit,
.token.statement,
.token.regex,
.token.atrule {
color: #d869f3;
}
.token.placeholder,
.token.variable {
color: #d869f3;
}
.token.deleted {
text-decoration: line-through;
}
.token.inserted {
border-bottom: 1px dotted #f5edff;
text-decoration: none;
}
.token.italic {
font-style: italic;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.important {
color: #b47ef6;
}
.token.entity {
cursor: help;
}
pre > code.highlight {
outline: .4em solid #7a4bb4;
outline-offset: .4em;
}
/* overrides color-values for the Line Numbers plugin
* http://prismjs.com/plugins/line-numbers/
*/
.line-numbers.line-numbers .line-numbers-rows {
border-right-color: #301f38;
}
.line-numbers .line-numbers-rows > span:before {
color: #412d52;
}
/* overrides color-values for the Line Highlight plugin
* http://prismjs.com/plugins/line-highlight/
*/
.line-highlight.line-highlight {
background: rgba(10, 163, 112, 0.2);
background: -webkit-linear-gradient(left, rgba(10, 163, 112, 0.2) 70%, rgba(10, 163, 112, 0));
background: linear-gradient(to right, rgba(10, 163, 112, 0.2) 70%, rgba(10, 163, 112, 0));
}
pre :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
color: hsl(270, 35%, 90%) !important;
background-color: transparent !important;
}

96
app/globals.css Normal file
View File

@@ -0,0 +1,96 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import './code.css';
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

38
app/layout.tsx Normal file
View File

@@ -0,0 +1,38 @@
import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { ThemeProvider } from "@/components/theme-provider"
import { Toaster } from "@/components/ui/toaster"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "TECHTONIC FAULT",
description: "Soluzioni software per aziende di qualsiasi dimensione.",
icons: {
icon: [
{
url: "/logo-icon.svg",
href: "/logo-icon.svg",
},
],
},
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="it" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange={false}>
{children}
<Toaster />
</ThemeProvider>
</body>
</html>
)
}

79
app/page.tsx Normal file
View File

@@ -0,0 +1,79 @@
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { MessageSquare, Phone, Users } from "lucide-react"
import Image from "next/image"
import { ContactForm } from "@/components/contact-form"
import { Testimonials, TestimonialsSection } from "@/components/testimonials"
import { Navbar } from "@/components/navbar"
import { Footer } from "@/components/footer"
import { HeroSection } from "@/components/hero-section"
import { ServicesSection } from "@/components/services-section"
import { ProjectsSection } from "@/components/projects-section"
import { BlogSection } from "@/components/blog-section"
import { Animate } from "@/components/animations/animate"
import { AboutSection } from "@/components/about-section"
export default function Home() {
return (
<div className="flex min-h-screen flex-col">
<Navbar />
<HeroSection />
<ServicesSection />
<ProjectsSection />
<AboutSection />
{/* Blog Section */}
<BlogSection />
{/* Testimonials Section */}
<TestimonialsSection />
{/* Contact Section */}
<section className="w-full py-12 md:py-24 lg:py-32 bg-white dark:bg-gray-950" id="contact">
<div className="container px-4 md:px-6 mx-auto">
<div className="grid gap-6 lg:grid-cols-2 lg:gap-12 items-start">
<div className="flex flex-col justify-center space-y-4">
<div className="space-y-2">
<Animate animation="fadeInUp" delay={0.1}>
<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl text-primary-900 dark:text-primary-400">
Contattaci
</h2>
</Animate>
<Animate animation="fadeInUp" delay={0.2}>
<p className="max-w-[600px] text-gray-500 dark:text-gray-400 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
Pronto a iniziare il tuo prossimo progetto? Contattaci per scoprire come possiamo aiutarti.
</p>
</Animate>
</div>
<div className="space-y-4">
{/* <Animate animation="fadeInUp" delay={0.3}>
<div className="flex items-center gap-3">
<Phone className="h-5 w-5 text-primary-900 dark:text-primary-400" />
<span className="text-gray-500 dark:text-gray-400">(555) 123-4567</span>
</div>
</Animate> */}
<Animate animation="fadeInUp" delay={0.4}>
<div className="flex items-center gap-3">
<MessageSquare className="h-5 w-5 text-primary-900 dark:text-primary-400" />
<span className="text-gray-500 dark:text-gray-400">info@techtonicfault.com</span>
</div>
</Animate>
<Animate animation="fadeInUp" delay={0.5}>
<div className="flex items-center gap-3">
<Users className="h-5 w-5 text-primary-900 dark:text-primary-400" />
<span className="text-gray-500 dark:text-gray-400">Pianifica una consulenza</span>
</div>
</Animate>
</div>
</div>
<div className="space-y-4">
<ContactForm />
</div>
</div>
</div>
</section>
<Footer />
</div>
)
}

View File

@@ -0,0 +1,13 @@
import type { Metadata } from "next"
import Privacy from '@/markdown/gymbro/privacy/en.mdx'
import RootLayout from "@/app/layout"
export const metadata: Metadata = {
title: "Gym Bro - Informativa sulla Privacy | TECHTONIC FAULT",
}
export default function BlogPage() {
return (
<Privacy />
)
}

View File

@@ -0,0 +1,13 @@
import type { Metadata } from "next"
import Privacy from '@/markdown/gymbro/privacy/it.mdx'
import RootLayout from "@/app/layout"
export const metadata: Metadata = {
title: "Gym Bro - Informativa sulla Privacy | TECHTONIC FAULT",
}
export default function BlogPage() {
return (
<Privacy />
)
}

14
app/pages/layout.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { Navbar } from "@/components/navbar";
import { Footer } from "@/components/footer";
export default function MdxLayout({ children }: { children: React.ReactNode }) {
// Create any shared layout or styles here
return (<div className="flex min-h-screen flex-col">
<Navbar />
<main className="flex-1 py-8">
<div className="container"><div className="prose dark:prose-invert">{children}</div></div>
</main>
<Footer />
</div>);
}

View File

@@ -0,0 +1,32 @@
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Navbar } from "@/components/navbar"
import { Footer } from "@/components/footer"
import { ArrowLeft } from "lucide-react"
export default function ProjectNotFound() {
return (
<div className="flex min-h-screen flex-col">
<Navbar />
<main className="flex-1 flex items-center justify-center">
<div className="container px-4 md:px-6 py-24 flex flex-col items-center text-center">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl text-primary-900 dark:text-primary-300 mb-4">
Progetto non trovato
</h1>
<p className="max-w-[600px] text-gray-500 dark:text-gray-400 md:text-xl/relaxed mb-8">
Il progetto che cercavi potrebbe essere stato eliminato o spostato.
</p>
<Link href="/portfolio">
<Button className="bg-primary-900 hover:bg-primary-800 dark:bg-primary-700 dark:hover:bg-primary-600">
<ArrowLeft />
Portfolio
</Button>
</Link>
</div>
</main>
<Footer />
</div>
)
}

View File

@@ -0,0 +1,129 @@
import { notFound } from "next/navigation"
import { Navbar } from "@/components/navbar"
import { Footer } from "@/components/footer"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { ArrowLeft, Calendar, Clock } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import type { Metadata } from "next"
import { MDXRemote } from "next-mdx-remote/rsc"
import rehypePrism from "rehype-prism-plus"
import projects from "@/data/portfolio"
interface BlogPostPageProps {
params: {
slug: string
}
}
export async function generateMetadata({ params }: BlogPostPageProps): Promise<Metadata> {
const p = await params;
const post = projects.find((post) => post._meta.path === p.slug)
if (!post) {
return {
title: "Progetto non trovato",
}
}
return {
title: `${post.title} | TECHTONIC FAULT Blog`,
description: post.description,
}
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const p = await params;
const post = projects.find((post) => post._meta.path === p.slug)
if (!post) {
notFound()
}
return (
<div className="flex min-h-screen flex-col">
<Navbar />
<main className="flex-1">
{/* Hero Section */}
<section className="w-full py-12 md:py-24 lg:py-32 bg-primary-50 dark:bg-gray-900">
<div className="container px-4 md:px-6 mx-auto">
<div className="flex flex-col items-center text-center space-y-4 max-w-3xl mx-auto">
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl text-primary-900 dark:text-primary-300">
{post.title}
</h1>
<div className="flex items-center justify-center gap-4 text-gray-500 dark:text-gray-400">
<div className="flex items-center">
<Calendar className="h-4 w-4 mr-2" />
{post.year}
</div>
</div>
<div className="flex flex-wrap gap-2 max-w-3xl mx-auto mt-12 mb-12">
{post.tags.map((tag, index) => (
<Badge
key={index}
variant="secondary"
className="bg-primary-100 text-primary-900 dark:bg-primary-900 dark:text-primary-100"
>
{tag}
</Badge>
))}
</div>
<p className="text-gray-500 dark:text-gray-400 mb-4">
{post.description}
</p>
</div>
</div>
</section>
{/* Featured Image */}
<div className="relative w-full h-[300px] md:h-[500px] lg:h-[600px]">
<Image src={post.image || "/placeholder.svg"} alt={post.title} fill className="object-cover" priority />
</div>
{/* Content */}
<article className="container px-4 md:px-6 mx-auto py-12 md:py-24">
<div className="prose prose-purple dark:prose-invert max-w-3xl mx-auto">
<MDXRemote
source={post.content}
options={{
mdxOptions: {
rehypePlugins: [rehypePrism],
},
}}
/>
{!!post.links && Object.values(post.links).some(v => !!v) && <div>
<h2>Scarica ora</h2>
<div className="flex flex-col sm:flex-row items-center gap-8">
{!!post.links.apple && <a href={post.links.apple} className="my-auto" target="_blank">
<Image alt="Download on the App Store" className="block dark:hidden h-16 w-auto" width={200} height={200} src="/appstore-light.svg" />
<Image alt="Download on the App Store" className="hidden dark:block h-16 w-auto" width={200} height={200} src="/appstore-dark.svg" />
</a>}
{!!post.links.play && <a href={post.links.play} className="my-auto" target="_blank"><Image alt="Download on Google Play Store" className="h-16 w-auto" width={200} height={200} src="/playstore.svg" /></a>}
</div>
</div>}
</div>
<div className="max-w-3xl mx-auto mt-12 pt-12 border-t border-gray-200 dark:border-gray-800">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
<Link href="/portfolio">
<Button
variant="outline"
className="border-primary-200 text-primary-900 dark:border-primary-800 dark:text-primary-300"
>
<ArrowLeft />
Portfolio
</Button>
</Link>
</div>
</div>
</article>
</main>
<Footer />
</div>
)
}

58
app/portfolio/page.tsx Normal file
View File

@@ -0,0 +1,58 @@
import { Navbar } from "@/components/navbar"
import { Footer } from "@/components/footer"
import { Badge } from "@/components/ui/badge"
import { BlogCard } from "@/components/blog-card"
import type { Metadata } from "next"
import { Animate } from "@/components/animations/animate"
import { Stagger } from "@/components/animations/stagger"
import { ProjectCard } from "@/components/project-card"
import projects from "@/data/portfolio"
export const metadata: Metadata = {
title: "Portfolio | TECHTONIC FAULT",
description: "Tutti i progetti ai quali abbiamo lavorato",
}
export default function BlogPage() {
return (
<div className="flex min-h-screen flex-col">
<Navbar />
<main className="flex-1">
{/* Hero Section */}
<section className="w-full py-12 md:py-24 lg:py-32 bg-primary-50 dark:bg-gray-900">
<div className="container px-4 md:px-6 mx-auto">
<div className="flex flex-col items-center text-center space-y-4">
<Animate animation="fadeInUp" delay={0.1}>
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl text-primary-900 dark:text-primary-300">
Portfolio
</h1>
</Animate>
<Animate animation="fadeInUp" delay={0.2}>
<p className="max-w-[900px] text-gray-500 dark:text-gray-400 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
Sfoglia i nostri lavori recenti e osserva come abbiamo aiutato i nostri partner a raggiungere i loro obiettivi.
</p>
</Animate>
</div>
</div>
</section>
{/* Blog Posts */}
<section className="w-full py-12 md:py-24 bg-white dark:bg-gray-950">
<div className="container px-4 md:px-6 mx-auto">
<Stagger className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" delay={0.3}>
{projects.map((project, index) => (
<ProjectCard
key={index}
project={project}
/>
))}
</Stagger>
</div>
</section>
</main>
<Footer />
</div>
)
}