From c482b6b2541ae288311c88e162d58039d241930e Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 14 May 2025 14:35:15 +0200 Subject: [PATCH] Initial commit --- .frontmatter/database/mediaDb.json | 1 + .frontmatter/database/pinnedItemsDb.json | 1 + .frontmatter/database/taxonomyDb.json | 1 + .gitignore | 30 + .vscode/settings.json | 12 + app/actions/contact.ts | 70 + app/blog/[slug]/not-found.tsx | 32 + app/blog/[slug]/page.tsx | 158 + app/blog/page.tsx | 64 + app/code.css | 180 + app/globals.css | 96 + app/layout.tsx | 38 + app/page.tsx | 79 + app/pages/gymbro/privacy/en/page.tsx | 13 + app/pages/gymbro/privacy/page.tsx | 13 + app/pages/layout.tsx | 14 + app/portfolio/[slug]/not-found.tsx | 32 + app/portfolio/[slug]/page.tsx | 129 + app/portfolio/page.tsx | 58 + components.json | 21 + components/about-section.tsx | 72 + components/animations/animate.tsx | 110 + components/animations/stagger.tsx | 109 + components/blog-card.tsx | 77 + components/blog-section.tsx | 47 + components/contact-form.tsx | 108 + components/footer.tsx | 95 + components/hero-section.module.css | 21 + components/hero-section.tsx | 67 + components/logo.tsx | 51 + components/navbar.tsx | 104 + components/project-card.tsx | 61 + components/projects-section.tsx | 47 + components/services-section.tsx | 43 + components/testimonials.tsx | 178 + components/theme-provider.tsx | 7 + components/theme-toggle.tsx | 28 + components/ui/accordion.tsx | 58 + components/ui/alert-dialog.tsx | 141 + components/ui/alert.tsx | 59 + components/ui/aspect-ratio.tsx | 7 + components/ui/avatar.tsx | 50 + components/ui/badge.tsx | 36 + components/ui/breadcrumb.tsx | 115 + components/ui/button.tsx | 56 + components/ui/calendar.tsx | 66 + components/ui/card.tsx | 79 + components/ui/carousel.tsx | 262 ++ components/ui/chart.tsx | 365 ++ components/ui/checkbox.tsx | 30 + components/ui/collapsible.tsx | 11 + components/ui/command.tsx | 153 + components/ui/context-menu.tsx | 200 + components/ui/dialog.tsx | 122 + components/ui/drawer.tsx | 118 + components/ui/dropdown-menu.tsx | 200 + components/ui/form.tsx | 178 + components/ui/hover-card.tsx | 29 + components/ui/input-otp.tsx | 71 + components/ui/input.tsx | 22 + components/ui/label.tsx | 26 + components/ui/menubar.tsx | 236 ++ components/ui/navigation-menu.tsx | 128 + components/ui/pagination.tsx | 117 + components/ui/popover.tsx | 31 + components/ui/progress.tsx | 28 + components/ui/radio-group.tsx | 44 + components/ui/resizable.tsx | 45 + components/ui/scroll-area.tsx | 48 + components/ui/select.tsx | 160 + components/ui/separator.tsx | 31 + components/ui/sheet.tsx | 140 + components/ui/sidebar.tsx | 763 ++++ components/ui/skeleton.tsx | 15 + components/ui/slider.tsx | 28 + components/ui/sonner.tsx | 31 + components/ui/switch.tsx | 29 + components/ui/table.tsx | 117 + components/ui/tabs.tsx | 55 + components/ui/textarea.tsx | 22 + components/ui/toast.tsx | 113 + components/ui/toaster.tsx | 24 + components/ui/toggle-group.tsx | 61 + components/ui/toggle.tsx | 45 + components/ui/tooltip.tsx | 30 + components/ui/use-mobile.tsx | 19 + components/ui/use-toast.ts | 189 + content-collections.ts | 90 + data/blog.ts | 7 + data/portfolio.ts | 6 + data/services.tsx | 34 + frontmatter.json | 52 + hooks/use-mobile.tsx | 19 + hooks/use-toast.ts | 194 + lib/blog.ts | 20 + lib/utils.ts | 6 + markdown/gymbro/privacy/en.mdx | 57 + markdown/gymbro/privacy/it.mdx | 57 + mdx-components.tsx | 7 + next.config.ts | 22 + package.json | 83 + pnpm-lock.yaml | 5 + postcss.config.mjs | 8 + public/.DS_Store | Bin 0 -> 6148 bytes public/appstore-dark.svg | 34 + public/appstore-light.svg | 34 + public/assets/portfolio/gym-bro/header.png | Bin 0 -> 366795 bytes public/assets/portfolio/mykennel/header.png | Bin 0 -> 678603 bytes public/assets/posts/logger/head.png | Bin 0 -> 106560 bytes public/grid.svg | 4 + public/hero.png | Bin 0 -> 3441668 bytes public/logo-dark.svg | 93 + public/logo-icon-dark.svg | 69 + public/logo-icon-mono.svg | 69 + public/logo-icon.svg | 69 + public/logo-mono.svg | 93 + public/logo.svg | 93 + public/placeholder-logo.png | Bin 0 -> 958 bytes public/placeholder-logo.svg | 1 + public/placeholder-user.jpg | Bin 0 -> 2615 bytes public/placeholder.jpg | Bin 0 -> 1596 bytes public/placeholder.svg | 1 + public/playstore.svg | 45 + src/authors/francesco.yml | 8 + src/portfolio/gym-bro.md | 62 + src/portfolio/mykennel.md | 38 + src/posts/logger.md | 146 + tailwind.config.ts | 179 + tsconfig.json | 28 + yarn.lock | 4168 +++++++++++++++++++ 130 files changed, 13171 insertions(+) create mode 100644 .frontmatter/database/mediaDb.json create mode 100644 .frontmatter/database/pinnedItemsDb.json create mode 100644 .frontmatter/database/taxonomyDb.json create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 app/actions/contact.ts create mode 100644 app/blog/[slug]/not-found.tsx create mode 100644 app/blog/[slug]/page.tsx create mode 100644 app/blog/page.tsx create mode 100644 app/code.css create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/pages/gymbro/privacy/en/page.tsx create mode 100644 app/pages/gymbro/privacy/page.tsx create mode 100644 app/pages/layout.tsx create mode 100644 app/portfolio/[slug]/not-found.tsx create mode 100644 app/portfolio/[slug]/page.tsx create mode 100644 app/portfolio/page.tsx create mode 100644 components.json create mode 100644 components/about-section.tsx create mode 100644 components/animations/animate.tsx create mode 100644 components/animations/stagger.tsx create mode 100644 components/blog-card.tsx create mode 100644 components/blog-section.tsx create mode 100644 components/contact-form.tsx create mode 100644 components/footer.tsx create mode 100644 components/hero-section.module.css create mode 100644 components/hero-section.tsx create mode 100644 components/logo.tsx create mode 100644 components/navbar.tsx create mode 100644 components/project-card.tsx create mode 100644 components/projects-section.tsx create mode 100644 components/services-section.tsx create mode 100644 components/testimonials.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/theme-toggle.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/aspect-ratio.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/calendar.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/chart.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/context-menu.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/drawer.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/resizable.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/toggle-group.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/ui/use-mobile.tsx create mode 100644 components/ui/use-toast.ts create mode 100644 content-collections.ts create mode 100644 data/blog.ts create mode 100644 data/portfolio.ts create mode 100644 data/services.tsx create mode 100644 frontmatter.json create mode 100644 hooks/use-mobile.tsx create mode 100644 hooks/use-toast.ts create mode 100644 lib/blog.ts create mode 100644 lib/utils.ts create mode 100644 markdown/gymbro/privacy/en.mdx create mode 100644 markdown/gymbro/privacy/it.mdx create mode 100644 mdx-components.tsx create mode 100644 next.config.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.mjs create mode 100644 public/.DS_Store create mode 100644 public/appstore-dark.svg create mode 100644 public/appstore-light.svg create mode 100644 public/assets/portfolio/gym-bro/header.png create mode 100644 public/assets/portfolio/mykennel/header.png create mode 100644 public/assets/posts/logger/head.png create mode 100644 public/grid.svg create mode 100644 public/hero.png create mode 100644 public/logo-dark.svg create mode 100644 public/logo-icon-dark.svg create mode 100644 public/logo-icon-mono.svg create mode 100644 public/logo-icon.svg create mode 100644 public/logo-mono.svg create mode 100644 public/logo.svg create mode 100644 public/placeholder-logo.png create mode 100644 public/placeholder-logo.svg create mode 100644 public/placeholder-user.jpg create mode 100644 public/placeholder.jpg create mode 100644 public/placeholder.svg create mode 100644 public/playstore.svg create mode 100644 src/authors/francesco.yml create mode 100644 src/portfolio/gym-bro.md create mode 100644 src/portfolio/mykennel.md create mode 100644 src/posts/logger.md create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 yarn.lock 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} + + )} + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +