mirror of
https://github.com/Techtonic-Fault/homepage.git
synced 2026-01-23 05:26:30 +00:00
Initial commit
This commit is contained in:
70
app/actions/contact.ts
Normal file
70
app/actions/contact.ts
Normal 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.",
|
||||
}
|
||||
}
|
||||
}
|
||||
32
app/blog/[slug]/not-found.tsx
Normal file
32
app/blog/[slug]/not-found.tsx
Normal 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
158
app/blog/[slug]/page.tsx
Normal 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
64
app/blog/page.tsx
Normal 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
180
app/code.css
Normal 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
96
app/globals.css
Normal 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
38
app/layout.tsx
Normal 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
79
app/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
13
app/pages/gymbro/privacy/en/page.tsx
Normal file
13
app/pages/gymbro/privacy/en/page.tsx
Normal 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 />
|
||||
)
|
||||
}
|
||||
13
app/pages/gymbro/privacy/page.tsx
Normal file
13
app/pages/gymbro/privacy/page.tsx
Normal 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
14
app/pages/layout.tsx
Normal 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>);
|
||||
}
|
||||
32
app/portfolio/[slug]/not-found.tsx
Normal file
32
app/portfolio/[slug]/not-found.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
129
app/portfolio/[slug]/page.tsx
Normal file
129
app/portfolio/[slug]/page.tsx
Normal 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
58
app/portfolio/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user