commit c482b6b2541ae288311c88e162d58039d241930e Author: Francesco Date: Wed May 14 14:35:15 2025 +0200 Initial commit diff --git a/.frontmatter/database/mediaDb.json b/.frontmatter/database/mediaDb.json new file mode 100644 index 0000000..9d09782 --- /dev/null +++ b/.frontmatter/database/mediaDb.json @@ -0,0 +1 @@ +{"public":{"assets":{"portfolio":{"mykennel":{}}}}} \ No newline at end of file diff --git a/.frontmatter/database/pinnedItemsDb.json b/.frontmatter/database/pinnedItemsDb.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.frontmatter/database/pinnedItemsDb.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.frontmatter/database/taxonomyDb.json b/.frontmatter/database/taxonomyDb.json new file mode 100644 index 0000000..2774907 --- /dev/null +++ b/.frontmatter/database/taxonomyDb.json @@ -0,0 +1 @@ +{"taxonomy":{"tags":["Astro","Firebase","Flutter","Frontend","Log","SQLite","Supabase"],"categories":[]}} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..19746fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# content-collections +.content-collections \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2a45cd0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "frontMatter.content.pageFolders": [ + { + "path": "[[workspace]]/src/posts", + "title": "posts" + }, + { + "title": "portfolio", + "path": "[[workspace]]/src/portfolio" + } + ] +} \ No newline at end of file diff --git a/app/actions/contact.ts b/app/actions/contact.ts new file mode 100644 index 0000000..6c8aa73 --- /dev/null +++ b/app/actions/contact.ts @@ -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 + +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.", + } + } +} diff --git a/app/blog/[slug]/not-found.tsx b/app/blog/[slug]/not-found.tsx new file mode 100644 index 0000000..4d73e71 --- /dev/null +++ b/app/blog/[slug]/not-found.tsx @@ -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 ( +
+ + +
+
+

+ Post non trovato +

+

+ Il post che cercavi potrebbe essere stato eliminato o spostato. +

+ + + +
+
+ +
+
+ ) +} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..f200977 --- /dev/null +++ b/app/blog/[slug]/page.tsx @@ -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 { + 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 ( +
+ + +
+ {/* Hero Section */} +
+
+
+ + {post.category} + +

+ {post.title} +

+
+
+ + {post.date} +
+
+ + {post.readTime} +
+
+
+ + + + {post.author.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

{post.author.name}

+

{post.author.title}

+
+
+
+
+
+ + {/* Featured Image */} +
+ {post.title} +
+ + {/* Content */} +
+
+ +
+ +
+ {post.tags.map((tag, index) => ( + + {tag} + + ))} +
+ +
+
+
+
+ + + + {post.author.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

{post.author.name}

+

{post.author.title}

+
+
+ + + +
+

+ {post.author.description} +

+
+
+
+
+ +
+
+ ) +} diff --git a/app/blog/page.tsx b/app/blog/page.tsx new file mode 100644 index 0000000..5584196 --- /dev/null +++ b/app/blog/page.tsx @@ -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 ( +
+ + +
+ {/* Hero Section */} +
+
+
+ +

+ Ultimi articoli +

+
+ +

+ Resta aggiornato con i nostri ultimi post su tecnologia, sviluppo e trend d'industria. +

+
+
+
+
+ + {/* Blog Posts */} +
+
+ + {blogPosts.map((post) => ( + + ))} + +
+
+ + {!blogPosts.length && <> +
+
+ + Non ci sono post. + +
+
+ } +
+ +
+
+ ) +} diff --git a/app/code.css b/app/code.css new file mode 100644 index 0000000..f4bc59d --- /dev/null +++ b/app/code.css @@ -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; +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..e5e5b77 --- /dev/null +++ b/app/globals.css @@ -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; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..08d8ec0 --- /dev/null +++ b/app/layout.tsx @@ -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 ( + + + + {children} + + + + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..2876907 --- /dev/null +++ b/app/page.tsx @@ -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 ( +
+ + + + + + + {/* Blog Section */} + + + {/* Testimonials Section */} + + + {/* Contact Section */} +
+
+
+
+
+ +

+ Contattaci +

+
+ +

+ Pronto a iniziare il tuo prossimo progetto? Contattaci per scoprire come possiamo aiutarti. +

+
+
+
+ {/* +
+ + (555) 123-4567 +
+
*/} + +
+ + info@techtonicfault.com +
+
+ +
+ + Pianifica una consulenza +
+
+
+
+
+ +
+
+
+
+ +
+
+ ) +} diff --git a/app/pages/gymbro/privacy/en/page.tsx b/app/pages/gymbro/privacy/en/page.tsx new file mode 100644 index 0000000..0fd2df1 --- /dev/null +++ b/app/pages/gymbro/privacy/en/page.tsx @@ -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 ( + + ) +} diff --git a/app/pages/gymbro/privacy/page.tsx b/app/pages/gymbro/privacy/page.tsx new file mode 100644 index 0000000..d7aa00c --- /dev/null +++ b/app/pages/gymbro/privacy/page.tsx @@ -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 ( + + ) +} diff --git a/app/pages/layout.tsx b/app/pages/layout.tsx new file mode 100644 index 0000000..bef4cc5 --- /dev/null +++ b/app/pages/layout.tsx @@ -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 (
+ +
+
{children}
+
+ +
+
); +} \ No newline at end of file diff --git a/app/portfolio/[slug]/not-found.tsx b/app/portfolio/[slug]/not-found.tsx new file mode 100644 index 0000000..721e629 --- /dev/null +++ b/app/portfolio/[slug]/not-found.tsx @@ -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 ( +
+ + +
+
+

+ Progetto non trovato +

+

+ Il progetto che cercavi potrebbe essere stato eliminato o spostato. +

+ + + +
+
+ +
+
+ ) +} diff --git a/app/portfolio/[slug]/page.tsx b/app/portfolio/[slug]/page.tsx new file mode 100644 index 0000000..06a2692 --- /dev/null +++ b/app/portfolio/[slug]/page.tsx @@ -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 { + 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 ( +
+ + +
+ {/* Hero Section */} +
+
+
+

+ {post.title} +

+
+
+ + {post.year} +
+
+
+ {post.tags.map((tag, index) => ( + + {tag} + + ))} +
+

+ {post.description} +

+
+
+
+ + {/* Featured Image */} +
+ {post.title} +
+ + {/* Content */} +
+
+ + {!!post.links && Object.values(post.links).some(v => !!v) &&
+

Scarica ora

+
+ {!!post.links.apple && + Download on the App Store + Download on the App Store + } + {!!post.links.play && Download on Google Play Store} +
+
} +
+ + +
+
+ + + +
+
+
+
+ +
+
+ ) +} diff --git a/app/portfolio/page.tsx b/app/portfolio/page.tsx new file mode 100644 index 0000000..dbc8d8f --- /dev/null +++ b/app/portfolio/page.tsx @@ -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 ( +
+ + +
+ {/* Hero Section */} +
+
+
+ +

+ Portfolio +

+
+ +

+ Sfoglia i nostri lavori recenti e osserva come abbiamo aiutato i nostri partner a raggiungere i loro obiettivi. +

+
+
+
+
+ + {/* Blog Posts */} +
+
+ + {projects.map((project, index) => ( + + ))} + +
+
+
+ +
+
+ ) +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..d9ef0ae --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/components/about-section.tsx b/components/about-section.tsx new file mode 100644 index 0000000..f609b4c --- /dev/null +++ b/components/about-section.tsx @@ -0,0 +1,72 @@ +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Animate } from "@/components/animations/animate" +import { Stagger } from "@/components/animations/stagger" +import Image from "next/image" + + +export function AboutSection() { + return ( +
+
+
+
+
+ +

+ La nostra storia +

+
+ +

+ Fondata con passione verso la tecnologia e la risoluzione di problemi logici, TECHTONIC FAULT + porta con sé anni di expertise nel settore. +

+
+
+
+ {/* +

+ With over a decade of experience in software development, I've worked with businesses of all sizes + to create custom solutions that drive growth and efficiency. +

+
*/} + +

+ Il nostro approccio combina esperienza tecnica e una profonda comprensione dei bisogni del + progetto, facendo in modo che esso possa raggiungere il massimo potenziale. +

+
+ +

+ Crediamo nella creazione di relazioni durature con i clienti, e forniamo supporto continuativo + al crescere dei loro bisogni tecnologici. +

+
+
+ {/* +
+ +
+
*/} +
+ {/* TODO: This */} + {/* +
+
+ About Us Image +
+
+
*/} +
+
+
+ ) +} diff --git a/components/animations/animate.tsx b/components/animations/animate.tsx new file mode 100644 index 0000000..9a31e18 --- /dev/null +++ b/components/animations/animate.tsx @@ -0,0 +1,110 @@ +"use client" + +import type React from "react" + +import { useEffect, useRef, useState } from "react" +import { motion, useAnimation, useInView } from "framer-motion" + +type AnimateProps = { + children: React.ReactNode + animation?: "fadeIn" | "fadeInUp" | "fadeInDown" | "fadeInLeft" | "fadeInRight" | "zoom" | "bounce" + delay?: number + duration?: number + className?: string + once?: boolean + threshold?: number +} + +const animations = { + fadeIn: { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + }, + fadeInUp: { + hidden: { opacity: 0, y: 50 }, + visible: { opacity: 1, y: 0 }, + }, + fadeInDown: { + hidden: { opacity: 0, y: -50 }, + visible: { opacity: 1, y: 0 }, + }, + fadeInLeft: { + hidden: { opacity: 0, x: -50 }, + visible: { opacity: 1, x: 0 }, + }, + fadeInRight: { + hidden: { opacity: 0, x: 50 }, + visible: { opacity: 1, x: 0 }, + }, + zoom: { + hidden: { opacity: 0, scale: 0.8 }, + visible: { opacity: 1, scale: 1 }, + }, + bounce: { + hidden: { opacity: 0, y: 50 }, + visible: { + opacity: 1, + y: 0, + transition: { + type: "spring", + bounce: 0.4, + }, + }, + }, +} + +export function Animate({ + children, + animation = "fadeIn", + delay = 0, + duration = 0.5, + className = "", + once = true, + threshold = 0.1, +}: AnimateProps) { + const controls = useAnimation() + const ref = useRef(null) + const isInView = useInView(ref, { once, threshold }) + const [shouldReduceMotion, setShouldReduceMotion] = useState(false) + + // Check for prefers-reduced-motion + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)") + setShouldReduceMotion(mediaQuery.matches) + + const handleChange = () => setShouldReduceMotion(mediaQuery.matches) + mediaQuery.addEventListener("change", handleChange) + return () => mediaQuery.removeEventListener("change", handleChange) + }, []) + + useEffect(() => { + if (isInView) { + controls.start("visible") + } else if (!once) { + controls.start("hidden") + } + }, [isInView, controls, once]) + + const selectedAnimation = animations[animation] + + // If user prefers reduced motion, use a simpler animation + const variants = shouldReduceMotion + ? { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + } + : selectedAnimation + + return ( + + {children} + + ) +} diff --git a/components/animations/stagger.tsx b/components/animations/stagger.tsx new file mode 100644 index 0000000..d2452d3 --- /dev/null +++ b/components/animations/stagger.tsx @@ -0,0 +1,109 @@ +"use client" + +import React from "react" + +import { useEffect, useRef, useState } from "react" +import { motion, useAnimation, useInView } from "framer-motion" + +type StaggerProps = { + children: React.ReactNode + delay?: number + staggerDelay?: number + duration?: number + className?: string + once?: boolean + threshold?: number + animation?: "fadeIn" | "fadeInUp" | "fadeInDown" | "fadeInLeft" | "fadeInRight" | "zoom" +} + +const animations = { + fadeIn: { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + }, + fadeInUp: { + hidden: { opacity: 0, y: 50 }, + visible: { opacity: 1, y: 0 }, + }, + fadeInDown: { + hidden: { opacity: 0, y: -50 }, + visible: { opacity: 1, y: 0 }, + }, + fadeInLeft: { + hidden: { opacity: 0, x: -50 }, + visible: { opacity: 1, x: 0 }, + }, + fadeInRight: { + hidden: { opacity: 0, x: 50 }, + visible: { opacity: 1, x: 0 }, + }, + zoom: { + hidden: { opacity: 0, scale: 0.8 }, + visible: { opacity: 1, scale: 1 }, + }, +} + +export function Stagger({ + children, + delay = 0, + staggerDelay = 0.1, + duration = 0.5, + className = "", + once = true, + threshold = 0.1, + animation = "fadeInUp", +}: StaggerProps) { + const controls = useAnimation() + const ref = useRef(null) + const isInView = useInView(ref, { once, threshold }) + const [shouldReduceMotion, setShouldReduceMotion] = useState(false) + + // Check for prefers-reduced-motion + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)") + setShouldReduceMotion(mediaQuery.matches) + + const handleChange = () => setShouldReduceMotion(mediaQuery.matches) + mediaQuery.addEventListener("change", handleChange) + return () => mediaQuery.removeEventListener("change", handleChange) + }, []) + + useEffect(() => { + if (isInView) { + controls.start("visible") + } else if (!once) { + controls.start("hidden") + } + }, [isInView, controls, once]) + + const selectedAnimation = animations[animation] + + // If user prefers reduced motion, use a simpler animation + const variants = shouldReduceMotion + ? { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + } + : selectedAnimation + + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: shouldReduceMotion ? 0 : staggerDelay, + delayChildren: delay, + }, + }, + } + + return ( + + {React.Children.map(children, (child) => ( + + {child} + + ))} + + ) +} diff --git a/components/blog-card.tsx b/components/blog-card.tsx new file mode 100644 index 0000000..9f920c3 --- /dev/null +++ b/components/blog-card.tsx @@ -0,0 +1,77 @@ +"use client" + +import { Card, CardContent, CardFooter } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { ArrowRight, Calendar, Clock } from "lucide-react" +import Image from "next/image" +import Link from "next/link" +import { motion } from "framer-motion" +import type { Post } from "content-collections" + +interface BlogCardProps { + post: Post +} + +export function BlogCard({ post }: BlogCardProps) { + return ( + + + +
+ + {post.title} + +
+ +
+ + {post.category} + +
+ + {post.date} +
+
+ + {post.readTime} +
+
+

{post.title}

+

{post.excerpt}

+
+ {post.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ + + +
+ +
+ ) +} diff --git a/components/blog-section.tsx b/components/blog-section.tsx new file mode 100644 index 0000000..6949949 --- /dev/null +++ b/components/blog-section.tsx @@ -0,0 +1,47 @@ +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { BlogCard } from "@/components/blog-card" +import { ArrowRight } from "lucide-react" +import Link from "next/link" +import { Animate } from "@/components/animations/animate" +import { Stagger } from "@/components/animations/stagger" +import blogPosts from "@/data/blog" + +export function BlogSection() { + if (!blogPosts.length) return <>; + + return ( +
+
+
+
+ +

+ Blog +

+
+ +

+ Resta aggiornato con i nostri ultimi post su tecnologia, sviluppo e trend d'industria. +

+
+
+
+ + {blogPosts.slice(0, 6).map((post) => ( + + ))} + + + + + + + +
+
+ ) +} diff --git a/components/contact-form.tsx b/components/contact-form.tsx new file mode 100644 index 0000000..378ed13 --- /dev/null +++ b/components/contact-form.tsx @@ -0,0 +1,108 @@ +"use client" + +import { useState, useRef } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { useToast } from "@/components/ui/use-toast" +import { submitContactForm } from "@/app/actions/contact" +import { useFormStatus } from "react-dom" +import { motion } from "framer-motion" + +function SubmitButton() { + const { pending } = useFormStatus() + + return ( + + ) +} + +export function ContactForm() { + const { toast } = useToast() + const formRef = useRef(null) + const [formState, setFormState] = useState<{ + success?: boolean + message?: string + }>({}) + + async function handleSubmit(formData: FormData) { + // Reset form state + setFormState({}) + + // Submit the form + const result = await submitContactForm(formData) + + // Update form state + setFormState(result) + + // Show toast notification + toast({ + title: result.success ? "Messaggio inviato!" : "Errore", + description: result.message, + variant: result.success ? "default" : "destructive", + }) + + // Reset form if successful + if (result.success && formRef.current) { + formRef.current.reset() + } + } + + return ( + +
+ {formState.message && ( + + {formState.message} + + )} + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +