initial commit

This commit is contained in:
Timo Knuth 2025-08-01 17:13:52 +02:00
commit 49bb62fc4e
47 changed files with 11431 additions and 0 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
.git
.gitignore
node_modules
.next
*.log

2
.flowbite-react/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
class-list.json
pid

View File

@ -0,0 +1,10 @@
{
"$schema": "https://unpkg.com/flowbite-react/schema.json",
"components": [],
"dark": true,
"path": "src/components",
"prefix": "",
"rsc": true,
"tsx": true,
"version": 3
}

22
.flowbite-react/init.tsx Normal file
View File

@ -0,0 +1,22 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore-all lint: auto-generated file
// This file is auto-generated by the flowbite-react CLI.
// Do not edit this file directly.
// Instead, edit the .flowbite-react/config.json file.
import { StoreInit } from "flowbite-react/store/init";
import React from "react";
export const CONFIG = {
dark: true,
prefix: "",
version: 3,
};
export function ThemeInit() {
return <StoreInit {...CONFIG} />;
}
ThemeInit.displayName = "ThemeInit";

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
.next

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"bradlc.vscode-tailwindcss"
]
}

21
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
"css.lint.unknownAtRules": "ignore",
"files.associations": {
"*.css": "tailwindcss"
},
"tailwindCSS.classAttributes": [
"class",
"className",
"theme"
],
"tailwindCSS.experimental.classRegex": [
[
"twMerge\\(([^)]*)\\)",
"[\"'`]([^\"'`]*).*?[\"'`]"
],
[
"createTheme(?:<\\w+>)?\\s*\\(([^)]*)\\)",
"{?\\s?[\\w].*:\\s*?[\"'`]([^\"'`]*).*?,?\\s?}?"
]
]
}

42
Dockerfile Normal file
View File

@ -0,0 +1,42 @@
# 1) Base
FROM node:18-alpine AS base
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
# 2) Deps
FROM base AS deps
RUN apk add --no-cache libc6-compat
COPY package.json package-lock.json ./
RUN npm ci
# 3) Builder
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
# falls public nicht existiert -> anlegen, damit COPY später nicht crasht
RUN mkdir -p public
RUN npm run build
RUN npm prune --omit=dev
# 4) Runner
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
PORT=3000 \
HOSTNAME=0.0.0.0
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
# Assets & Build-Output kopieren
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER nextjs
EXPOSE 3000
CMD ["node_modules/.bin/next", "start", "-p", "3000"]

233
Home.jsx Normal file
View File

@ -0,0 +1,233 @@
import React, { useState, useEffect, useMemo, useCallback } from "react";
import { motion } from "framer-motion";
import SEOHead from "./components/SEOHead";
import MobileOptimizedHeader from "./components/MobileOptimizedHeader";
import EnhancedTextInput from "./components/EnhancedTextInput";
import ImprovedCategoryFilter from "./components/ImprovedCategoryFilter";
import PerformanceOptimizedFontCard from "./components/PerformanceOptimizedFontCard";
import InfoSection from "./components/InfoSection";
import SocialButtons from "./components/SocialButtons";
import {
fontTransforms,
getFontsByCategory,
transformText,
getPopularFonts,
} from "./components/FontTransforms";
// kleine Helper
const sArr = (v) => (Array.isArray(v) ? v : []);
const sStr = (v) => (v ?? "").toString();
const sObj = (v) => (v ?? {});
export default function Home() {
const [inputText, setInputText] = useState("Hello Instagram!");
const [selectedCategory, setSelectedCategory] = useState("all");
const [isMobile, setIsMobile] = useState(false);
const [animationsEnabled, setAnimationsEnabled] = useState(() => {
if (typeof window !== "undefined") {
const hasReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const cores = typeof navigator !== "undefined" ? navigator.hardwareConcurrency : 8;
const isLowEndDevice = (cores ?? 8) < 4;
return !hasReducedMotion && !isLowEndDevice;
}
return true;
});
// Mobile Detection
useEffect(() => {
const checkMobile = () => {
if (typeof window !== "undefined") {
setIsMobile(
window.innerWidth < 768 ||
/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
);
}
};
checkMobile();
if (typeof window !== "undefined") {
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}
}, []);
// Debounce
const [debouncedText, setDebouncedText] = useState(inputText);
useEffect(() => {
const delay = isMobile ? 200 : 100;
const t = setTimeout(() => setDebouncedText(inputText), delay);
return () => clearTimeout(t);
}, [inputText, isMobile]);
// Fonts
const availableFonts = useMemo(
() => sArr(getFontsByCategory?.(selectedCategory)),
[selectedCategory]
);
const popularFonts = useMemo(() => sArr(getPopularFonts?.()), []);
const fontCounts = useMemo(() => {
const totalFonts = Object.keys(sObj(fontTransforms)).length;
const counts = { all: totalFonts };
Object.values(sObj(fontTransforms)).forEach((font) => {
const cat = font?.category ?? "other";
counts[cat] = (counts[cat] || 0) + 1;
});
return counts;
}, []);
// Analytics
const handleFontCopy = useCallback((fontName, text) => {
if (typeof window !== "undefined" && window.gtag) {
window.gtag("event", "font_copied", {
font_name: fontName,
text_length: sStr(text).length,
category: fontTransforms?.[fontName]?.category,
});
}
}, []);
const handleQuickShare = useCallback(async () => {
if (typeof window === "undefined") return;
const shareData = {
title: "FancyText - Cool Fonts! 🔥",
text: "Check out this app for cool Instagram & TikTok fonts! 30+ fonts for free ✨",
url: window.location.href,
};
if (navigator.share) {
try {
await navigator.share(shareData);
} catch (err) {
if (err.name !== "AbortError") console.error("Share failed:", err);
}
} else {
try {
await navigator.clipboard.writeText(`${shareData.text}\n${shareData.url}`);
alert("Link copied! 📋");
} catch (err) {
console.error("Copy failed:", err);
}
}
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-pink-500 via-purple-600 to-blue-600 relative overflow-hidden">
<SEOHead currentText={inputText} />
{/* Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500&family=Bebas+Neue&family=Poppins:wght@500&family=Pacifico&family=Dancing+Script&family=Anton&family=Orbitron&family=Playfair+Display&display=swap"
rel="stylesheet"
/>
{/* Background blobs */}
{animationsEnabled && !isMobile && (
<>
<div className="absolute top-20 left-10 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-blob"></div>
<div className="absolute top-40 right-10 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-blob animation-delay-2000"></div>
<div className="absolute -bottom-8 left-20 w-72 h-72 bg-blue-300 rounded-full mix-blend-multiply filter blur-xl opacity-30 animate-blob animation-delay-4000"></div>
</>
)}
<div className="relative z-10 container mx-auto px-2 sm:px-4 py-4 sm:py-8 max-w-6xl">
<MobileOptimizedHeader
animationsEnabled={animationsEnabled}
onToggleAnimations={setAnimationsEnabled}
totalFonts={Object.keys(sObj(fontTransforms)).length}
onQuickShare={handleQuickShare}
/>
<EnhancedTextInput
text={inputText}
onChange={setInputText}
placeholder="✍️ Enter your text:"
/>
<ImprovedCategoryFilter
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
fontCounts={fontCounts}
isMobile={isMobile}
/>
{/* Font Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 mb-8 sm:mb-12">
{availableFonts.map((fontName, index) => (
<PerformanceOptimizedFontCard
key={fontName}
fontName={fontName}
transformedText={transformText(sStr(debouncedText), fontName) ?? ""}
category={fontTransforms?.[fontName]?.category ?? "other"}
isPopular={popularFonts.includes(fontName)}
animationsEnabled={animationsEnabled}
index={index}
onCopy={handleFontCopy}
/>
))}
</div>
<SocialButtons onShare={handleQuickShare} />
<InfoSection />
<footer className="text-center text-white/60 text-xs sm:text-sm mt-8 sm:mt-12 space-y-2 px-4">
<p>
© 2024 FancyText - The best <strong>font generator for Instagram</strong> and{" "}
<strong>TikTok</strong>
</p>
<p className="text-xs">
Create cool fonts for social media posts {" "}
{Object.keys(sObj(fontTransforms)).length}+ unique fonts
</p>
<div className="flex flex-wrap justify-center gap-2 sm:gap-4 mt-4 text-xs text-white/40">
<span>SEO-optimized</span>
<span></span>
<span>Mobile-first</span>
<span></span>
<span>Performance-optimized</span>
<span></span>
<span>PWA-ready</span>
</div>
</footer>
</div>
<style jsx>{`
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.touch-manipulation {
touch-action: manipulation;
}
@keyframes blob {
0% { transform: translate(0px, 0px) scale(1); }
33% { transform: translate(30px, -50px) scale(1.1); }
66% { transform: translate(-20px, 20px) scale(0.9); }
100% { transform: translate(0px, 0px) scale(1); }
}
.animate-blob { animation: blob 7s infinite; }
.animation-delay-2000 { animation-delay: 2s; }
.animation-delay-4000 { animation-delay: 4s; }
@media (hover: hover) {
.hover-lift:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
}
}
@media (max-width: 640px) {
.text-responsive {
font-size: 1.25rem;
line-height: 1.4;
}
}
@media (display-mode: standalone) {
body { padding-top: env(safe-area-inset-top); }
}
`}</style>
</div>
);
}

View File

@ -0,0 +1,41 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
const categories = [
{ key: 'all', label: 'Alle', icon: '🎨' },
{ key: 'modern', label: 'Modern', icon: '🔤' },
{ key: 'handschrift', label: 'Handschrift', icon: '✍️' },
{ key: 'statement', label: 'Statement', icon: '🧑‍🎤' },
{ key: 'futuristisch', label: 'Futuristisch', icon: '🚀' },
{ key: 'aesthetic', label: 'Aesthetic', icon: '🧢' }
];
export default function CategoryFilter({ selectedCategory, onCategoryChange, fontCounts }) {
return (
<div className="mb-8">
<div className="flex flex-wrap gap-2 justify-center">
{categories.map((category) => (
<Button
key={category.key}
variant={selectedCategory === category.key ? "default" : "outline"}
onClick={() => onCategoryChange(category.key)}
className={`rounded-full transition-all duration-300 ${
selectedCategory === category.key
? 'bg-gradient-to-r from-pink-500 to-purple-500 text-white shadow-lg scale-105'
: 'bg-white/20 border-white/30 text-white hover:bg-white/30 hover:scale-105'
}`}
>
<span className="mr-2">{category.icon}</span>
{category.label}
{fontCounts[category.key] && (
<Badge variant="secondary" className="ml-2 bg-white/20 text-white">
{fontCounts[category.key]}
</Badge>
)}
</Button>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,139 @@
import React, { useRef, useState } from "react";
import { motion } from "framer-motion";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Type, X, Sparkles, Shuffle } from "lucide-react";
export default function EnhancedTextInput({
text: _text,
inputText,
onChange: _onChange,
onTextChange,
placeholder,
onRandomFont,
}) {
const text = _text ?? inputText ?? "";
const change = _onChange ?? onTextChange ?? (() => {});
const safeLen = (v) => (typeof v === "string" ? v.length : 0);
const inputRef = useRef(null);
const [focused, setFocused] = useState(false);
const quickTexts = [
"Hello Instagram! 👋",
"Follow me! ✨",
"New Post 🔥",
"Story Time 📖",
"Good Vibes Only ✌️",
];
const handleClear = () => {
change("");
inputRef.current?.focus();
};
const handleQuickText = (qt) => {
change(qt);
inputRef.current?.focus();
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="mb-6 sm:mb-8"
>
{/* Stylischer Button über dem Textfeld */}
<div className="flex justify-start mb-4">
<Button
onClick={onRandomFont}
className="bg-pink-600 hover:bg-pink-700 text-white text-sm font-semibold px-4 py-2 rounded-xl shadow-md flex items-center gap-2 transition-all duration-200 hover:scale-105"
>
<Shuffle className="w-4 h-4" />
Try Random Font
</Button>
</div>
{/* Eingabe */}
<div className="relative mb-4">
<div
className={`relative bg-white/95 backdrop-blur-sm rounded-2xl transition-all duration-300 ${
focused
? "ring-2 ring-pink-400 shadow-xl scale-[1.02]"
: "shadow-lg hover:shadow-xl"
}`}
>
<div className="flex items-center p-1">
<div className="flex items-center justify-center w-12 h-12 bg-gradient-to-br from-pink-500 to-purple-600 rounded-xl mr-3">
<Type className="w-5 h-5 text-white" />
</div>
<Input
ref={inputRef}
value={text}
onChange={(e) => change(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder={placeholder}
className="flex-1 border-0 bg-transparent text-gray-800 text-lg font-medium placeholder:text-gray-400 focus-visible:ring-0 focus-visible:ring-offset-0"
maxLength={100}
/>
{!!safeLen(text) && (
<Button
onClick={handleClear}
variant="ghost"
size="sm"
style={{ pointerEvents: "auto" }}
className="mr-2 border border-black/20 hover:border-black/40 hover:bg-black/10 text-black rounded-full w-8 h-8 p-0 transition"
aria-label="Clear text"
>
<X className="w-4 h-4" />
</Button>
)}
</div>
</div>
{/* Counter */}
<span className="absolute -bottom-1 right-4 text-xs text-black font-medium">
{safeLen(text)}/100
</span>
</div>
{/* Quick Examples */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="mb-4"
>
<div className="flex items-center gap-2 mb-3">
<Sparkles className="w-4 h-4 text-yellow-300" />
<span className="text-white/80 text-sm font-medium">Quick Examples:</span>
</div>
<div className="flex flex-wrap gap-2">
{quickTexts.map((qt) => (
<Button
key={qt}
onClick={() => handleQuickText(qt)}
variant="outline"
size="sm"
className="bg-white/10 border-white/20 text-white hover:bg-white/20 backdrop-blur-sm text-xs transition-all duration-200 hover:scale-105"
>
{qt}
</Button>
))}
</div>
</motion.div>
{/* USP / SEO-Text */}
<div className="text-sm text-white/80 text-center space-y-1 mt-6">
<p>📱 Optimized for Instagram, TikTok, Threads & WhatsApp</p>
<p>🔍 SEO-safe Unicode 100% copy-paste ready</p>
<p>🚀 Built for speed & mobile-first UX</p>
</div>
</motion.div>
);
}

58
components/Header.jsx Normal file
View File

@ -0,0 +1,58 @@
import React from "react";
import { Sparkles } from "lucide-react";
export default function Header() {
return (
<header className="text-center mb-8 space-y-4 relative">
{/* 🔗 About + Privacy Links oben rechts (aktuell vollständig versteckt) */}
<div
className="fixed top-4 right-4 z-50 flex gap-6 text-sm"
aria-hidden="true"
style={{ display: "none" }} // 🔧 Sichtbar machen = einfach löschen
>
<a
href="#"
onClick={(e) => {
e.preventDefault();
document.getElementById("about")?.scrollIntoView({ behavior: "smooth" });
window.history.replaceState(null, '', window.location.pathname);
}}
className="text-white hover:underline cursor-pointer"
>
About
</a>
<a
href="#"
onClick={(e) => {
e.preventDefault();
document.getElementById("privacy")?.scrollIntoView({ behavior: "smooth" });
window.history.replaceState(null, '', window.location.pathname);
}}
className="text-white hover:underline cursor-pointer"
>
Privacy
</a>
</div>
{/* 🔥 Logo + Titel */}
<div className="flex items-center justify-center gap-3 mb-4 mt-[250px]">
<Sparkles className="w-8 h-8 text-yellow-300 animate-pulse" />
<h1 className="text-4xl md:text-5xl font-black text-white tracking-tight">
FancyText
</h1>
<Sparkles className="w-8 h-8 text-yellow-300 animate-pulse" />
</div>
<p className="text-xl text-white/90 font-medium mb-2">
Coole Schriftarten für Instagram & TikTok
</p>
<p className="text-white/80 mb-6">
30+ Fancy Fonts zum Kopieren perfekt für deine Bio
</p>
</header>
);
}

View File

@ -0,0 +1,125 @@
import React from "react";
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
export default function ImprovedCategoryFilter({
selectedCategory,
onCategoryChange,
fontCounts,
isMobile
}) {
const categories = [
{
id: 'all',
name: '🔥 All Fonts',
description: 'Complete collection',
gradient: 'from-pink-500 to-purple-600'
},
{
id: 'modern',
name: '🔤 Modern',
description: 'Clean & professional',
gradient: 'from-blue-500 to-indigo-600'
},
{
id: 'handwriting',
name: '✍️ Handwriting',
description: 'Personal & casual',
gradient: 'from-green-500 to-emerald-600'
},
{
id: 'statement',
name: '🧑‍🎤 Statement',
description: 'Bold & eye-catching',
gradient: 'from-red-500 to-pink-600'
},
{
id: 'futuristic',
name: '🚀 Futuristic',
description: 'Tech & gaming',
gradient: 'from-purple-500 to-violet-600'
},
{
id: 'aesthetic',
name: '🧢 Aesthetic',
description: 'Retro & Instagram vibes',
gradient: 'from-yellow-500 to-orange-600'
}
];
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-8"
>
{/* Section Header */}
<div className="text-center mb-6">
<h2 className="text-2xl font-bold text-white mb-2">Choose Your Style</h2>
<p className="text-white/70 text-sm">
Browse fonts by category or view all {fontCounts.all} unique styles
</p>
</div>
{/* Category Grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
{categories.map((category, index) => (
<motion.div
key={category.id}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1 * index }}
>
<Button
onClick={() => onCategoryChange(category.id)}
variant={selectedCategory === category.id ? "default" : "outline"}
className={`w-full h-auto p-3 relative overflow-hidden transition-all duration-300 ${
selectedCategory === category.id
? `bg-gradient-to-br ${category.gradient} text-white shadow-lg ring-2 ring-white/30 scale-105`
: 'bg-white/10 border-white/20 text-white hover:bg-white/20 hover:scale-105'
} backdrop-blur-sm rounded-xl group`}
>
{/* Background Glow Effect */}
{selectedCategory === category.id && (
<div className={`absolute inset-0 bg-gradient-to-br ${category.gradient} opacity-20 blur-xl`} />
)}
<div className="relative z-10 text-center">
<div className="text-lg mb-1">{category.name}</div>
<div className="text-xs opacity-80 mb-2">{category.description}</div>
<Badge
variant={selectedCategory === category.id ? "secondary" : "outline"}
className={`text-xs ${
selectedCategory === category.id
? 'bg-white/20 text-white border-white/30'
: 'bg-white/10 text-white/80 border-white/20'
}`}
>
{fontCounts[category.id] || 0} fonts
</Badge>
</div>
</Button>
</motion.div>
))}
</div>
{/* Active Category Info */}
{selectedCategory !== 'all' && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="mt-4 text-center"
>
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-3 inline-block">
<p className="text-white/80 text-sm">
Showing <span className="font-semibold text-white">{fontCounts[selectedCategory]}</span> fonts
in <span className="font-semibold text-white">{categories.find(c => c.id === selectedCategory)?.name}</span> category
</p>
</div>
</motion.div>
)}
</motion.div>
);
}

268
components/InfoSection.jsx Normal file
View File

@ -0,0 +1,268 @@
// components/InfoSection.jsx
import React from "react";
import { motion } from "framer-motion";
import { Card } from "@/components/ui/card";
import {
Copy,
Smartphone,
Zap,
Heart,
Instagram,
Music,
MessageCircle,
Sparkles
} from "lucide-react";
export default function InfoSection({ currentText = "Hello Instagram!" }) {
const features = [
{
icon: <Copy className="w-5 h-5 text-blue-500" />,
title: "Instant Copy",
description: "One-tap copying to clipboard. No complicated steps."
},
{
icon: <Smartphone className="w-5 h-5 text-green-500" />,
title: "Mobile First",
description: "Optimized for phones. Works perfectly on all devices."
},
{
icon: <Zap className="w-5 h-5 text-yellow-500" />,
title: "Lightning Fast",
description: "Instant preview as you type. No loading times."
},
{
icon: <Heart className="w-5 h-5 text-red-500" />,
title: "Completely Free",
description: "All 30+ fonts are free forever. No hidden costs."
}
];
const platforms = [
{
icon: <Instagram className="w-6 h-6 text-pink-500" />,
name: "Instagram",
description: "Stories, Bio, Posts"
},
{
icon: <Music className="w-6 h-6 text-black" />,
name: "TikTok",
description: "Comments, Bio, Videos"
},
{
icon: <MessageCircle className="w-6 h-6 text-green-500" />,
name: "WhatsApp",
description: "Status, Messages"
},
{
icon: <Sparkles className="w-6 h-6 text-purple-500" />,
name: "Everywhere",
description: "Any social platform"
}
];
return (
<motion.section
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mb-12 space-y-20"
>
<div id="about">
<h2 className="text-3xl font-bold text-black mb-4">
📱 Vorschau in Social Media
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<Card className="p-6 bg-white border border-gray-200 rounded-xl shadow-sm flex flex-col justify-between">
<div>
<h3 className="text-black font-semibold mb-1">Instagram Bio</h3>
<div className="text-sm text-black-600 mb-2">
<span className="font-semibold text-black">@your_username</span><br />
Content Creator | 📍 Berlin<br />
🖋 fancytextstuff.com
</div>
</div>
<div className="bg-white rounded-xl p-4 text-black shadow-sm flex-1 flex items-center justify-center">
<p className="text-2xl font-bold">{currentText}</p>
</div>
</Card>
<Card className="p-6 bg-white border border-gray-200 rounded-xl shadow-sm flex flex-col justify-between">
<div>
<h3 className="text-black font-semibold mb-1">TikTok Comment</h3>
<div className="text-sm text-black-600 mb-2">
<span className="font-semibold text-black">@your_username</span><br />
Content Creator | 📍 Berlin <br />
🖋 fancytextstuff.com
</div>
</div>
<div className="bg-white rounded-xl p-4 text-black shadow-sm flex-1 flex items-center justify-center">
<p className="text-2xl font-bold">{currentText}</p>
</div>
</Card>
</div>
</div>
<div className="h-8" />
<Card className="bg-white/95 backdrop-blur-sm p-6 sm:p-8 border-0 shadow-xl">
<h2 className="text-2xl sm:text-3xl font-bold text-gray-800 text-center mb-6">
How FancyText Works
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{features.map((feat, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 * idx }}
className="text-center"
>
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
{feat.icon}
</div>
<h3 className="font-semibold text-gray-800 mb-2">{feat.title}</h3>
<p className="text-gray-600 text-sm">{feat.description}</p>
</motion.div>
))}
</div>
<div className="bg-gradient-to-r from-pink-50 to-purple-50 rounded-xl p-6">
<h3 className="font-bold text-gray-800 mb-4 text-center">3 Simple Steps:</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ step: "1", text: "Type your text above", emoji: "⌨️" },
{ step: "2", text: "Pick a font style you like", emoji: "🎨" },
{ step: "3", text: "Tap to copy & paste anywhere", emoji: "📋" },
].map((item, idx) => (
<div key={idx} className="text-center">
<div className="text-2xl mb-2">{item.emoji}</div>
<div className="bg-white rounded-lg p-3">
<div className="w-6 h-6 bg-pink-500 text-white rounded-full flex items-center justify-center text-sm font-bold mx-auto mb-2">
{item.step}
</div>
<p className="text-gray-700 text-sm font-medium">{item.text}</p>
</div>
</div>
))}
</div>
</div>
</Card>
<div className="h-8" />
<Card className="bg-white/95 backdrop-blur-sm p-6 sm:p-8 border-0 shadow-xl">
<h2 className="text-2xl sm:text-3xl font-bold text-gray-800 text-center mb-6">
Works Everywhere
</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
{platforms.map((plat, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1 * idx }}
className="bg-white rounded-xl p-4 text-center shadow-md hover:shadow-lg transition-all duration-200 hover:scale-105"
>
<div className="mb-3 flex justify-center">{plat.icon}</div>
<h3 className="font-semibold text-gray-800 mb-1">{plat.name}</h3>
<p className="text-gray-600 text-xs">{plat.description}</p>
</motion.div>
))}
</div>
<div className="text-center">
<p className="text-gray-600 text-sm">
<strong>Universal compatibility:</strong> Our fonts work on Instagram, TikTok, WhatsApp, Twitter,
Facebook, Discord, and any platform that supports Unicode text.
</p>
</div>
</Card>
<div className="h-8" />
<Card className="bg-white/95 backdrop-blur-sm p-6 sm:p-8 border-0 shadow-xl">
<h2 className="text-2xl sm:text-3xl font-bold text-gray-800 text-center mb-6">
Frequently Asked Questions
</h2>
<div className="space-y-4">
{[
{
q: "Are these fonts really free?",
a: "Yes! All 30+ fonts are completely free to use. No registration, no payments, no limits."
},
{
q: "Will these fonts work on my phone?",
a: "Absolutely! Our fonts are Unicode-based and work on all devices iPhone, Android, tablets, and computers."
},
{
q: "Can I use these for commercial purposes?",
a: "Yes, you can use our generated text for personal and commercial social media posts, stories, and bios."
},
{
q: "Why do I need FancyFonts?",
a: "Fancy fonts help your posts stand out, increase engagement, and make your social media presence more memorable."
}
].map((faq, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 * idx }}
className="bg-gray-50 rounded-lg p-4"
>
<h3 className="font-semibold text-gray-800 mb-2">Q: {faq.q}</h3>
<p className="text-gray-600 text-sm">A: {faq.a}</p>
</motion.div>
))}
</div>
</Card>
<div className="h-8" />
<div
id="privacy"
className="bg-gray-900 text-white border border-white rounded-xl shadow-lg max-w-3xl mx-auto px-6 py-8 mb-[20px]"
>
<h2 className="text-2xl font-bold mb-4 text-center">Privacy Policy</h2>
<div className="space-y-4 text-sm leading-relaxed">
<p>
Protecting your data is important to us.
</p>
<p>
This website uses Google AdSense to display advertisements. Google may use cookies or similar technologies to tailor ads to your interests. In doing so, data like your IP address may be processed and potentially transferred to servers outside the EU. This only happens with your consent via the cookie banner (Art. 6 (1) a GDPR in conjunction with § 25 TTDSG). You can change or withdraw your consent at any time.
</p>
<p>
<strong>No data collection with font features</strong><br />
All font generation and formatting runs entirely in your browser. We do not store personal data or use cookies for this purpose.
</p>
<p>
<strong>Anonymous usage statistics</strong><br />
To improve our service, we may collect anonymous statistics without storing IP addresses or other personal information.
</p>
<p>
<strong>SSL encryption</strong><br />
Our website uses SSL encryption to ensure your data is securely transmitted.
</p>
<p>
<strong>Contact</strong><br />
For any questions about privacy:<br />
📧 <a
href="mailto:support@yourdomain.com"
className="text-blue-400 underline"
>
support@yourdomain.com
</a>
</p>
</div>
</div>
<div className="text-center mt-8 text-sm text-gray-400">
© 2025 FancyTextStuff
</div>
</motion.section>
);
}

View File

@ -0,0 +1,61 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Share2, Info } from "lucide-react";
export default function MobileOptimizedHeader({
totalFonts,
onQuickShare
}) {
return (
<header className="text-center mb-6 sm:mb-8" style={{ pointerEvents: "none" }}>
{/* Main Title */}
<div className="mb-4">
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-2 tracking-tight">
<span className="bg-gradient-to-r from-yellow-300 via-pink-300 to-purple-300 bg-clip-text text-transparent">
FancyText
</span>
<span className="text-2xl sm:text-3xl ml-2">🔥</span>
</h1>
<p className="text-white/90 text-base sm:text-lg font-medium mb-1">
Make Your Bio Go Viral 🔥
</p>
<p className="text-white/70 text-sm">
{totalFonts}+ unique fonts Copy & Paste ready
</p>
</div>
{/* Controls Row */}
<div className="flex flex-wrap items-center justify-center gap-3 sm:gap-4 mb-4">
{/* Quick Share */}
<Button
onClick={onQuickShare}
variant="outline"
size="sm"
className="bg-white/10 border-white/20 text-white hover:bg-white/20 backdrop-blur-sm"
style={{ pointerEvents: "auto" }}
>
<Share2 className="w-4 h-4 mr-1" />
Share App
</Button>
{/* Info Badge */}
<div className="flex items-center gap-1 bg-green-500/20 backdrop-blur-sm rounded-full px-3 py-1">
<Info className="w-3 h-3 text-green-300" />
<span className="text-green-200 text-xs font-medium">Free & Fast</span>
</div>
</div>
{/* Feature Pills */}
<div className="flex flex-wrap items-center justify-center gap-2 text-xs">
{["📱 Mobile Optimized", "⚡ Instant Copy", "🎨 30+ Styles", "🔄 Always Updated"].map((feature, index) => (
<span
key={index}
className="bg-white/10 backdrop-blur-sm text-white/80 px-2 py-1 rounded-full"
>
{feature}
</span>
))}
</div>
</header>
);
}

View File

@ -0,0 +1,198 @@
import React, { useState, useCallback, forwardRef, memo } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Copy, Check, Heart, Share2, Info, Zap } from "lucide-react";
import { fontTransforms } from "@/components/fontTransforms";
const sStr = (v) => (v ?? "").toString();
const updateFontHistory = (fontName) => {
if (typeof window === "undefined") return;
try {
const key = "fancytext_recent_fonts";
const stored = JSON.parse(sessionStorage.getItem(key) || "[]");
const updated = [fontName, ...stored.filter((f) => f !== fontName)].slice(0, 5);
sessionStorage.setItem(key, JSON.stringify(updated));
} catch {}
};
const PerformanceOptimizedFontCard = forwardRef(({
fontName,
transformedText,
category,
isPopular,
animationsEnabled,
index,
onCopy,
onLike,
onShare
}, ref) => {
const [copied, setCopied] = useState(false);
const [liked, setLiked] = useState(() => {
if (typeof window === "undefined") return false;
try {
return localStorage.getItem(`liked_${fontName}`) === "true";
} catch {
return false;
}
});
const handleCopy = useCallback(async () => {
const textToCopy = sStr(transformedText);
try {
await navigator.clipboard.writeText(textToCopy);
setCopied(true);
updateFontHistory(fontName);
navigator.vibrate?.(50);
onCopy?.(fontName, textToCopy);
setTimeout(() => setCopied(false), 2000);
} catch {
const textarea = document.createElement("textarea");
textarea.value = textToCopy;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
setCopied(true);
updateFontHistory(fontName);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
console.error("Fallback copy failed:", e);
}
document.body.removeChild(textarea);
}
}, [transformedText, fontName, onCopy]);
const handleLike = useCallback(() => {
const newLiked = !liked;
setLiked(newLiked);
try { localStorage.setItem(`liked_${fontName}`, newLiked.toString()); } catch {}
navigator.vibrate?.(newLiked ? 30 : 0);
onLike?.(fontName, newLiked);
}, [liked, fontName, onLike]);
const handleShare = useCallback(async () => {
const shareText = `${sStr(transformedText)}\n\nErstellt mit FancyText: ${window.location.href}`;
if (navigator.share) {
try {
await navigator.share({ title: "Schau dir diese coole Schriftart an! 🔥", text: shareText, url: window.location.href });
onShare?.(fontName);
} catch {}
} else {
try {
await navigator.clipboard.writeText(shareText);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
console.error("Share fallback failed:", e);
}
}
}, [transformedText, fontName, onShare]);
const getFontStyle = useCallback((name) => {
const baseStyle = { wordBreak: "break-word", lineHeight: "1.3", willChange: "auto" };
const fontMap = {
Montserrat: { fontFamily: "Montserrat, sans-serif", fontWeight: "500" },
'Bebas Neue': { fontFamily: '"Bebas Neue", cursive', fontWeight: "400", textTransform: "uppercase", letterSpacing: "0.05em" },
Oswald: { fontFamily: "Oswald, sans-serif", fontWeight: "500", textTransform: "uppercase" },
Raleway: { fontFamily: "Raleway, sans-serif", fontWeight: "400" },
Poppins: { fontFamily: "Poppins, sans-serif", fontWeight: "500" },
Inter: { fontFamily: "Inter, sans-serif", fontWeight: "400" },
Caveat: { fontFamily: "Caveat, cursive", fontWeight: "400" },
Pacifico: { fontFamily: "Pacifico, cursive", fontWeight: "400" },
'Dancing Script': { fontFamily: '"Dancing Script", cursive', fontWeight: "400" },
'Amatic SC': { fontFamily: '"Amatic SC", cursive', fontWeight: "400" },
Anton: { fontFamily: "Anton, sans-serif", fontWeight: "400", textTransform: "uppercase" },
'Luckiest Guy': { fontFamily: '"Luckiest Guy", cursive', fontWeight: "400", textTransform: "uppercase" },
'Fredoka One': { fontFamily: '"Fredoka One", cursive', fontWeight: "400" },
Bangers: { fontFamily: "Bangers, cursive", fontWeight: "400", textTransform: "uppercase" },
Orbitron: { fontFamily: "Orbitron, sans-serif", fontWeight: "400" },
'Press Start 2P': { fontFamily: '"Press Start 2P", cursive', fontWeight: "400", fontSize: "0.85em" },
'Playfair Display': { fontFamily: '"Playfair Display", serif', fontWeight: "400" }
};
return { ...baseStyle, ...(fontMap[name] || {}) };
}, []);
const previewText = sStr(transformedText) || "Hallo Instagram!";
return (
<div ref={ref} className="will-change-transform mb-6">
<Card className="bg-white/95 backdrop-blur-sm border-0 shadow-xl hover:shadow-2xl transition-all duration-200 overflow-hidden touch-manipulation">
<div className="p-4 sm:p-6">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2 min-w-0 flex-1">
<h3 className="font-semibold text-gray-800 truncate text-sm sm:text-base">{fontName}</h3>
{isPopular && (
<Badge className="bg-gradient-to-r from-pink-500 to-purple-500 text-white text-xs shrink-0">
<Zap className="w-3 h-3 mr-1" /> Top
</Badge>
)}
</div>
<div className="flex items-center gap-1 shrink-0">
<Button
variant="ghost"
size="sm"
onClick={handleLike}
style={{ pointerEvents: "auto" }}
className={`p-2 touch-manipulation ${liked ? "text-pink-500" : "text-gray-400"}`}
aria-label={liked ? "Unlike font" : "Like font"}
>
<Heart className={`w-4 h-4 ${liked ? "fill-current" : ""}`} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleShare}
style={{ pointerEvents: "auto" }}
className="p-2 touch-manipulation text-gray-400 hover:text-blue-500"
aria-label="Share font"
>
<Share2 className="w-4 h-4" />
</Button>
</div>
</div>
{fontTransforms[fontName]?.description && (
<p className="text-xs text-gray-500 mb-3 flex items-start gap-1 leading-tight">
<Info className="w-3 h-3 mt-0.5 shrink-0" />
{fontTransforms[fontName].description}
</p>
)}
<div
onClick={handleCopy}
role="button"
tabIndex={0}
onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && handleCopy()}
aria-label="Click to copy text"
style={{ ...getFontStyle(fontName), pointerEvents: "auto" }}
className="text-xl sm-text-2xl md-text-3xl mb-4 p-3 sm:p-4 bg-gray-50 rounded-xl text-center select-all text-gray-800 min-h-[70px] sm:min-h-[80px] flex items-center justify-center cursor-pointer hover:bg-gray-100 transition-colors"
>
{previewText}
</div>
<Button
onClick={handleCopy}
disabled={copied}
style={{ pointerEvents: "auto" }}
className={`w-full transition-all duration-200 touch-manipulation text-white font-medium py-3 rounded-xl shadow-lg hover:shadow-xl active:scale-95 ${
copied
? "bg-green-500 hover:bg-green-600 shadow-green-200"
: "bg-gradient-to-r from-pink-500 to-purple-500 hover:from-pink-600 hover:to-purple-600 shadow-pink-200"
}`}
>
{copied ? (
<><Check className="w-4 h-4 mr-2" /> Copy! </>
) : (
<><Copy className="w-4 h-4 mr-2" /> Copy! </>
)}
</Button>
</div>
</Card>
</div>
);
});
PerformanceOptimizedFontCard.displayName = "PerformanceOptimizedFontCard";
export default memo(PerformanceOptimizedFontCard);

103
components/SEOHead.jsx Normal file
View File

@ -0,0 +1,103 @@
import React from "react";
export default function SEOHead({ currentText = "Hello Instagram!" }) {
const title = "FancyText | viral Fonts 🔥";
const description =
"Transform your Instagram & TikTok text in seconds. 30+ viral fonts. 100% free, mobile-ready & creator-approved. Copy & Paste.";
const keywords =
"fancy text generator, instagram fonts, tiktok fonts, whatsapp fonts, cool fonts, bio generator, fancy fonts, text generator, instagram bio, social media fonts, copy paste fonts, viral font styles";
React.useEffect(() => {
document.title = title;
const setMetaTag = (name, content, property = false) => {
const attr = property ? "property" : "name";
let meta = document.querySelector(`meta[${attr}="${name}"]`);
if (!meta) {
meta = document.createElement("meta");
meta.setAttribute(attr, name);
document.head.appendChild(meta);
}
meta.setAttribute("content", content);
};
setMetaTag("description", description);
setMetaTag("keywords", keywords);
setMetaTag("viewport", "width=device-width, initial-scale=1.0, viewport-fit=cover");
setMetaTag("theme-color", "#E91E63");
setMetaTag("apple-mobile-web-app-capable", "yes");
setMetaTag("apple-mobile-web-app-status-bar-style", "black-translucent");
setMetaTag("apple-mobile-web-app-title", "FancyText");
// Open Graph Tags (Social Sharing)
setMetaTag("og:type", "website", true);
setMetaTag("og:url", "https://yourdomain.com", true);
setMetaTag("og:title", "FancyText 🔥 Make Your Text Stand Out on IG & TikTok", true);
setMetaTag("og:description", "30+ copy-paste font styles used by top creators. Mobile optimized, always free. Try now.", true);
setMetaTag("og:image", "https://deineseite.com/fancytextstuff_icon.png", true);
// Twitter Tags
setMetaTag("twitter:card", "summary_large_image", true);
setMetaTag("twitter:url", "https://yourdomain.com", true);
setMetaTag("twitter:title", title, true);
setMetaTag("twitter:description", description, true);
setMetaTag("twitter:image", "https://deineseite.com/fancytextstuff_icon.png", true);
const addPreconnect = (href, crossorigin = false) => {
if (!document.querySelector(`link[href="${href}"]`)) {
const link = document.createElement("link");
link.rel = "preconnect";
link.href = href;
if (crossorigin) link.crossOrigin = "anonymous";
document.head.appendChild(link);
}
};
addPreconnect("https://fonts.googleapis.com");
addPreconnect("https://fonts.gstatic.com", true);
const addStructuredData = () => {
const existingScript = document.getElementById("structured-data");
if (!existingScript) {
const script = document.createElement("script");
script.id = "structured-data";
script.type = "application/ld+json";
script.textContent = JSON.stringify({
"@context": "https://schema.org",
"@type": "WebApplication",
name: "FancyText",
headline: "Boost Your Instagram Look in 10 Seconds 🔥",
alternativeHeadline: "30+ Viral Fonts Copy & Paste Ready. Works Everywhere.",
description: description,
keywords: [
"instagram fonts",
"copy paste fonts",
"fancy text generator",
"tiktok fonts",
"bio fonts",
"cool font styles",
],
url: "https://yourdomain.com",
applicationCategory: "UtilitiesApplication",
operatingSystem: "Any",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
},
author: {
"@type": "Organization",
name: "FancyText",
},
});
document.head.appendChild(script);
}
};
addStructuredData();
}, [title, description]);
return null;
}

View File

@ -0,0 +1,44 @@
import React from "react";
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { Share2 } from "lucide-react";
export default function SocialButtons() {
const handleShare = async () => {
const shareData = {
title: "FancyText - Teile die App! 🔥",
text: "Check out FancyText coole Fonts für Instagram, TikTok & Co.!",
url: window.location.href,
};
if (navigator.share) {
try {
await navigator.share(shareData);
} catch (err) {
if (err.name !== "AbortError") console.error("Share failed:", err);
}
} else {
try {
await navigator.clipboard.writeText(`${shareData.text}\n${shareData.url}`);
alert("Link kopiert! 📋");
} catch (err) {
console.error("Copy failed:", err);
}
}
};
return (
<motion.section
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="mb-8 sm:mb-12 text-center"
>
<Button
onClick={handleShare}
className="inline-flex items-center bg-gradient-to-br from-blue-500 to-indigo-500 text-white px-6 py-3 rounded-xl shadow-lg hover:opacity-90 transition"
>
<Share2 className="w-5 h-5 mr-2" /> Share
</Button>
</motion.section>
);
}

21
components/TextInput.jsx Normal file
View File

@ -0,0 +1,21 @@
import React from "react";
import { Type } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
export default function TextInput({ text, onChange, placeholder = "Gib deinen Text ein:" }) {
return (
<Card className="bg-white/10 backdrop-blur-lg border border-white/20 p-6 mb-8">
<div className="flex items-center gap-3 mb-4">
<Type className="w-5 h-5 text-white" />
<h2 className="text-lg font-semibold text-white">{placeholder}</h2>
</div>
<Input
value={text}
onChange={(e) => onChange(e.target.value)}
placeholder="Hallo Instagram!"
className="bg-white/20 border-white/30 text-white placeholder:text-white/60 text-lg py-6 px-4 rounded-xl focus:bg-white/30 transition-all duration-300"
/>
</Card>
);
}

View File

@ -0,0 +1,86 @@
// components/fontTransforms.jsx
// Unicode-basiertes Font-Transformationsmapping für deine aktuelle Font-Liste
// Nutzt verschiedene Unicode-Blöcke, damit beim Kopieren der "fancy" Stil erhalten bleibt.
// 1) Definition der Unicode-Blöcke (Startpunkte)
const unicodeBlocks = {
sansSerif: { upperStart: 0x1D5A0, lowerStart: 0x1D5BA }, // Mathematical Sans-Serif
sansSerifBold: { upperStart: 0x1D5D4, lowerStart: 0x1D5EE }, // Bold Sans-Serif
script: { upperStart: 0x1D49C, lowerStart: 0x1D4B6 }, // Mathematical Script
scriptBold: { upperStart: 0x1D4D0, lowerStart: 0x1D4EA }, // Bold Script
fraktur: { upperStart: 0x1D504, lowerStart: 0x1D51E }, // Mathematical Fraktur
frakturBold: { upperStart: 0x1D56C, lowerStart: 0x1D586 }, // Bold Fraktur
monospace: { upperStart: 0x1D670, lowerStart: 0x1D68A }, // Mathematical Monospace
fullwidth: { upperStart: 0xFF21, lowerStart: 0xFF41 } // Fullwidth Latin
};
// 2) Helfer zum Mappen von A-Z und a-z in den jeweiligen Unicode-Block
const mapUnicode = (char, block) => {
const code = char.charCodeAt(0);
if (code >= 65 && code <= 90) return String.fromCodePoint(block.upperStart + (code - 65));
if (code >= 97 && code <= 122) return String.fromCodePoint(block.lowerStart + (code - 97));
return char;
};
const createTransform = (blockKey) => (text) =>
text
.split('')
.map((c) => mapUnicode(c, unicodeBlocks[blockKey]))
.join('');
// 3) Font-Transformations für deine Liste
export const fontTransforms = {
// 🔤 Modern Clean & professional
Montserrat: { transform: createTransform('sansSerifBold'), category: 'modern', description: 'Montserrat Sans-Serif Bold Unicode' },
Lato: { transform: createTransform('sansSerif'), category: 'modern', description: 'Lato Humanistischer Sans-Serif Unicode' },
Raleway: { transform: createTransform('sansSerif'), category: 'modern', description: 'Raleway Elegant Display Unicode' },
Poppins: { transform: createTransform('sansSerif'), category: 'modern', description: 'Poppins Rund & freundlich Unicode' },
'Open Sans': { transform: createTransform('sansSerif'), category: 'modern', description: 'Open Sans Vielseitig Unicode' },
Roboto: { transform: createTransform('sansSerif'), category: 'modern', description: 'Roboto Modernes Grotesk Unicode' },
'Work Sans': { transform: createTransform('sansSerif'), category: 'modern', description: 'Work Sans Tech & Clean Unicode' },
// Handwriting Personal & casual
Pacifico: { transform: createTransform('scriptBold'), category: 'handwriting', description: 'Pacifico Lockerer Pinsel Bold Script Unicode' },
Sacramento: { transform: createTransform('scriptBold'), category: 'handwriting', description: 'Sacramento Retro-Handlettering Bold Script Unicode' },
Caveat: { transform: createTransform('scriptBold'), category: 'handwriting', description: 'Caveat Natural Handwriting Bold Script Unicode' },
'Dancing Script': { transform: createTransform('scriptBold'), category: 'handwriting', description: 'Dancing Script Lebhafte Kursive Bold Script Unicode' },
'Indie Flower': { transform: createTransform('scriptBold'), category: 'handwriting', description: 'Indie Flower Verspieltes Bold Script Unicode' },
'Amatic SC': { transform: createTransform('scriptBold'), category: 'handwriting', description: 'Amatic SC Skizzenartiges Bold Script Unicode' },
'Kaushan Script': { transform: createTransform('scriptBold'), category: 'handwriting', description: 'Kaushan Script Fettere Kursive Bold Script Unicode' },
// 🧑🎤 Statement Bold & eye-catching
Oswald: { transform: createTransform('sansSerifBold'), category: 'statement', description: 'Oswald Bold Grotesk Unicode' },
'Bebas Neue': { transform: createTransform('fullwidth'), category: 'statement', description: 'Bebas Neue Fullwidth Caps Unicode' },
Anton: { transform: createTransform('fullwidth'), category: 'statement', description: 'Anton Plakative Fullwidth Unicode' },
Ultra: { transform: createTransform('sansSerifBold'), category: 'statement', description: 'Ultra Kompakte Bold Unicode' },
'Stint Ultra Condensed': { transform: createTransform('sansSerifBold'), category: 'statement', description: 'Stint Ultra Condensed Kompakte Bold Unicode' },
'Playfair Display': { transform: createTransform('scriptBold'), category: 'statement', description: 'Playfair Display Elegante Bold Script Unicode' },
'Abril Fatface': { transform: createTransform('scriptBold'), category: 'statement', description: 'Abril Fatface Fettere Bold Script Unicode' },
// 🚀 Futuristic Tech & gaming
Exo: { transform: createTransform('sansSerif'), category: 'futuristic', description: 'Exo Tech Grotesk Unicode' },
Orbitron: { transform: createTransform('monospace'), category: 'futuristic', description: 'Orbitron Sci-Fi Monospace Unicode' },
Audiowide: { transform: createTransform('monospace'), category: 'futuristic', description: 'Audiowide Rundes Monospace Unicode' },
Rajdhani: { transform: createTransform('monospace'), category: 'futuristic', description: 'Rajdhani Digital Monospace Unicode' },
'Space Mono': { transform: createTransform('monospace'), category: 'futuristic', description: 'Space Mono Tech Monospace Unicode' },
Questrial: { transform: createTransform('sansSerif'), category: 'futuristic', description: 'Questrial Clean Sans-Serif Unicode' },
// 🧢 Aesthetic Retro & Instagram vibes
'Press Start 2P': { transform: createTransform('monospace'), category: 'aesthetic', description: 'Press Start 2P Pixel Monospace Unicode' },
Righteous: { transform: createTransform('frakturBold'), category: 'aesthetic', description: 'Righteous Stylische Bold Fraktur Unicode' },
'Metal Mania': { transform: createTransform('scriptBold'), category: 'aesthetic', description: 'Metal Mania Fettere Script Unicode' }
};
// Hilfsfunktionen
export const getPopularFonts = () => Object.keys(fontTransforms).slice(0, 10);
export const getFontsByCategory = (category) => (
category === 'all'
? Object.keys(fontTransforms)
: Object.keys(fontTransforms).filter(f => fontTransforms[f].category === category)
);
export const transformText = (text, fontName) => {
const font = fontTransforms[fontName];
if (!font || !text) return text;
return font.transform(text);
};

154
components/ui/FontCard.jsx Normal file
View File

@ -0,0 +1,154 @@
// components/ui/FontCard.jsx
import React, { useState } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Copy, Check, Heart, Share2, Info } from "lucide-react";
import { fontTransforms } from "../fontTransforms";
import { getFontData } from "@/lib/fonts";
export default function FontCard({
fontName,
transformedText,
category,
isPopular,
index = 0,
}) {
const [copied, setCopied] = useState(false);
const [liked, setLiked] = useState(false);
const fontInfo = fontTransforms[fontName];
const fontData = getFontData(fontName);
const displayText = transformedText || "Hallo Instagram!";
const handleCopy = () => {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
.writeText(displayText)
.then(() => flashCopied())
.catch(() => fallbackCopy());
} else {
fallbackCopy();
}
};
const flashCopied = () => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const fallbackCopy = () => {
const textarea = document.createElement("textarea");
textarea.value = displayText;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.top = "0";
textarea.style.left = "0";
textarea.style.width = "1px";
textarea.style.height = "1px";
textarea.style.padding = "0";
textarea.style.border = "none";
textarea.style.outline = "none";
textarea.style.boxShadow = "none";
textarea.style.background = "transparent";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand("copy");
flashCopied();
} catch (err) {
console.error("Fallback Copy fehlgeschlagen:", err);
}
document.body.removeChild(textarea);
};
const handleShare = async () => {
if (!navigator.share) return;
try {
await navigator.share({
title: `FancyText ${fontName}`,
text: displayText,
url: window.location.href,
});
} catch (err) {
console.error("Share fehlgeschlagen:", err);
}
};
return (
<div style={{ pointerEvents: "none" }}>
<Card className="bg-white/90 backdrop-blur-sm border-0 shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-gray-800 capitalize">{fontName}</h3>
{isPopular && (
<Badge className="bg-gradient-to-r from-pink-500 to-purple-500 text-white text-[10px] uppercase tracking-wide">
Beliebt
</Badge>
)}
{category && (
<Badge className="bg-gray-200 text-gray-600 text-[10px] uppercase tracking-wide">
{category}
</Badge>
)}
</div>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setLiked(!liked)}
className={liked ? "text-pink-500" : "text-gray-400"}
>
<Heart className={`w-4 h-4 ${liked ? "fill-current" : ""}`} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleShare}
className="text-gray-400 hover:text-blue-500"
>
<Share2 className="w-4 h-4" />
</Button>
</div>
</div>
{fontInfo?.description && (
<p className="text-xs text-gray-500 mb-3 flex items-center gap-1">
<Info className="w-3 h-3" />
{fontInfo.description}
</p>
)}
<input
type="text"
value={displayText}
readOnly
className={`${fontData.className} text-2xl md:text-3xl mb-6 p-4 bg-gray-50 rounded-xl text-center text-gray-800 min-h-[80px] w-full select-all border-0 focus:ring-0`}
style={{ lineHeight: "1.2" }}
/>
<Button
onClick={handleCopy}
className="w-full bg-gradient-to-r from-pink-500 to-purple-500 hover:from-pink-600 hover:to-purple-600 text-white font-medium py-3 rounded-xl shadow-lg pointer-events-auto"
disabled={copied}
>
{copied ? (
<>
<Check className="w-4 h-4 mr-2" />
Copy!
</>
) : (
<>
<Copy className="w-4 h-4 mr-2" />
Copy now
</>
)}
</Button>
</div>
</Card>
</div>
);
}

8
components/ui/badge.jsx Normal file
View File

@ -0,0 +1,8 @@
import React from "react";
export const Badge = ({ className = "", ...props }) => (
<span
className={`inline-block text-xs px-2 py-1 rounded bg-black/20 text-white ${className}`}
{...props}
/>
);

8
components/ui/button.jsx Normal file
View File

@ -0,0 +1,8 @@
import React from "react";
export const Button = ({ as: Comp = "button", className = "", ...props }) => (
<Comp
className={`px-4 py-2 rounded-md font-medium border hover:opacity-90 transition ${className}`}
{...props}
/>
);

8
components/ui/card.jsx Normal file
View File

@ -0,0 +1,8 @@
import React from "react";
export const Card = ({ className = "", ...props }) => (
<div
className={`rounded-xl border p-4 bg-white/10 backdrop-blur-sm text-white ${className}`}
{...props}
/>
);

12
components/ui/input.jsx Normal file
View File

@ -0,0 +1,12 @@
import React from "react";
export const Input = React.forwardRef(({ className = "", ...props }, ref) => {
return (
<input
ref={ref}
className={`px-3 py-2 rounded-md border w-full focus:outline-none focus:ring ${className}`}
{...props}
/>
);
});
Input.displayName = "Input";

19
components/ui/switch.jsx Normal file
View File

@ -0,0 +1,19 @@
import React from "react";
export const Switch = ({ checked = false, onChange, className = "" }) => (
<label className={`inline-flex items-center cursor-pointer ${className}`}>
<input
type="checkbox"
className="sr-only"
checked={checked}
onChange={(e) => onChange?.(e.target.checked)}
/>
<span className="relative w-10 h-5 bg-gray-400 rounded-full transition">
<span
className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full transition-transform ${
checked ? "translate-x-5" : ""
}`}
/>
</span>
</label>
);

8
docker-compose.yml Normal file
View File

@ -0,0 +1,8 @@
version: "3.8"
services:
fancytext-app:
build: .
ports:
- "3001:3000"
environment:
- NODE_ENV=production

40
entities/FontStyle.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "FontStyle",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name der Schriftart"
},
"category": {
"type": "string",
"enum": [
"serif",
"sans-serif",
"decorative",
"script",
"monospace",
"outlined",
"special"
],
"description": "Kategorie der Schriftart"
},
"css_transform": {
"type": "string",
"description": "CSS transformation or unicode mapping"
},
"unicode_map": {
"type": "object",
"description": "Unicode character mapping for transformation"
},
"popular": {
"type": "boolean",
"default": false,
"description": "Beliebte Schriftart hervorheben"
}
},
"required": [
"name",
"category"
]
}

9
jsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
"forceConsistentCasingInFileNames": true
}
}

153
lib/fonts.js Normal file
View File

@ -0,0 +1,153 @@
// 1) GoogleFonts Platzhalter
export const inter = { className: "", variable: "--font-inter" };
export const roboto = { className: "", variable: "--font-roboto" };
export const openSans = { className: "", variable: "--font-opensans" };
export const montserrat = { className: "", variable: "--font-montserrat" };
export const raleway = { className: "", variable: "--font-raleway" };
export const poppins = { className: "", variable: "--font-poppins" };
export const manrope = { className: "", variable: "--font-manrope" };
export const dmSans = { className: "", variable: "--font-dmsans" };
export const plusJakartaSans = {
className: "",
variable: "--font-plusjakarta",
};
export const spaceGrotesk = { className: "", variable: "--font-spacegrotesk" };
export const dancingScript = {
className: "",
variable: "--font-dancingscript",
};
export const pacifico = { className: "", variable: "--font-pacifico" };
export const caveat = { className: "", variable: "--font-caveat" };
export const indieFlower = { className: "", variable: "--font-indieflower" };
export const greatVibes = { className: "", variable: "--font-greatvibes" };
export const sacramento = { className: "", variable: "--font-sacramento" };
export const alexBrush = { className: "", variable: "--font-alexbrush" };
export const amaticSC = { className: "", variable: "--font-amaticsc" };
export const marckScript = { className: "", variable: "--font-marckscript" };
export const protestRevolution = {
className: "",
variable: "--font-protestrevolution",
};
export const anton = { className: "", variable: "--font-anton" };
export const bebasNeue = { className: "", variable: "--font-bebasneue" };
export const oswald = { className: "", variable: "--font-oswald" };
export const bangers = { className: "", variable: "--font-bangers" };
export const ultra = { className: "", variable: "--font-ultra" };
export const abrilFatface = { className: "", variable: "--font-abrilfatface" };
export const fjallaOne = { className: "", variable: "--font-fjallaone" };
export const fredokaOne = { className: "", variable: "--font-fredokaone" };
export const luckiestGuy = { className: "", variable: "--font-luckiestguy" };
export const fugazOne = { className: "", variable: "--font-fugazone" };
export const shrikhand = { className: "", variable: "--font-shrikhand" };
export const chango = { className: "", variable: "--font-chango" };
export const gravitasOne = { className: "", variable: "--font-gravitasone" };
export const coiny = { className: "", variable: "--font-coiny" };
export const quicksand = { className: "", variable: "--font-quicksand" };
export const orbitron = { className: "", variable: "--font-orbitron" };
export const zenDots = { className: "", variable: "--font-zendots" };
export const audiowide = { className: "", variable: "--font-audiowide" };
export const exo2 = { className: "", variable: "--font-exo2" };
export const rajdhani = { className: "", variable: "--font-rajdhani" };
export const syncopate = { className: "", variable: "--font-syncopate" };
export const pressStart2p = { className: "", variable: "--font-pressstart2p" };
export const shareTechMono = {
className: "",
variable: "--font-sharetechmono",
};
export const playfairDisplay = {
className: "",
variable: "--font-playfairdisplay",
};
export const cinzel = { className: "", variable: "--font-cinzel" };
export const italiana = { className: "", variable: "--font-italiana" };
export const youngSerif = { className: "", variable: "--font-youngserif" };
export const caprasimo = { className: "", variable: "--font-caprasimo" };
export const righteous = { className: "", variable: "--font-righteous" };
export const luxuriousRoman = {
className: "",
variable: "--font-luxuriousroman",
};
export const vt323 = { className: "", variable: "--font-vt323" };
export const neonderthaw = { className: "", variable: "--font-neonderthaw" };
// 2) SystemFonts
export const systemFonts = {
helvetica: { className: "font-helvetica", variable: "--font-helvetica" },
arial: { className: "font-arial", variable: "--font-arial" },
comicSans: { className: "font-comicsans", variable: "--font-comicsans" },
};
// 3) PseudoFonts
export const pseudoFonts = {
bubble: { className: "", variable: "--font-bubble" },
glitch: { className: "", variable: "--font-glitch" },
wide: { className: "", variable: "--font-wide" },
upsideDown: { className: "", variable: "--font-upsidedown" },
strikethrough: { className: "", variable: "--font-strikethrough" },
underline: { className: "", variable: "--font-underline" },
};
// Zusammenfassung aller Fonts
export const fonts = {
Inter: inter,
Roboto: roboto,
Open_Sans: openSans,
Montserrat: montserrat,
Raleway: raleway,
Poppins: poppins,
Manrope: manrope,
DM_Sans: dmSans,
Plus_Jakarta_Sans: plusJakartaSans,
Space_Grotesk: spaceGrotesk,
Dancing_Script: dancingScript,
Pacifico: pacifico,
Caveat: caveat,
Indie_Flower: indieFlower,
Great_Vibes: greatVibes,
Sacramento: sacramento,
Alex_Brush: alexBrush,
Amatic_SC: amaticSC,
Marck_Script: marckScript,
Protest_Revolution: protestRevolution,
Anton: anton,
Bebas_Neue: bebasNeue,
Oswald: oswald,
Bangers: bangers,
Ultra: ultra,
Abril_Fatface: abrilFatface,
Fjalla_One: fjallaOne,
Fredoka_One: fredokaOne,
Luckiest_Guy: luckiestGuy,
Fugaz_One: fugazOne,
Shrikhand: shrikhand,
Chango: chango,
Gravitas_One: gravitasOne,
Coiny: coiny,
Quicksand: quicksand,
Orbitron: orbitron,
Zen_Dots: zenDots,
Audiowide: audiowide,
Exo_2: exo2,
Rajdhani: rajdhani,
Syncopate: syncopate,
Press_Start_2P: pressStart2p,
Share_Tech_Mono: shareTechMono,
Playfair_Display: playfairDisplay,
Cinzel: cinzel,
Italiana: italiana,
Young_Serif: youngSerif,
Caprasimo: caprasimo,
Righteous: righteous,
Luxurious_Roman: luxuriousRoman,
VT323: vt323,
Neonderthaw: neonderthaw,
...systemFonts,
...pseudoFonts,
};
export const getFontData = (key) => fonts[key] ?? inter;

View File

@ -0,0 +1,14 @@
/**
* Enthält **nur** die CSSVariablenNamen, die next/font im Browser setzt.
* In fonts.js werden sie erzeugt, hier referenzieren wir sie nur.
*/
export default {
montserrat: "--font-montserrat",
bebasneue: "--font-bebasneue",
pacifico: "--font-pacifico",
caveat: "--font-caveat",
fredokaone: "--font-fredokaone",
playfair: "--font-playfair",
vt323: "--font-vt323",
// … alle weiteren Fonts, die du brauchst
};

9
next.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// Deine sonstigen NextOptionen, z.B.:
// reactStrictMode: true,
// images: { domains: [...] },
// rewrites: async () => [ ... ],
};
module.exports = nextConfig;

12
next.config.mjs Normal file
View File

@ -0,0 +1,12 @@
// next.config.mjs
import path from "path";
import withFlowbiteReact from "flowbite-react/plugin/nextjs";
const nextConfig = {
webpack: (config) => {
config.resolve.alias["@"] = path.resolve(process.cwd());
return config;
},
};
export default withFlowbiteReact(nextConfig);

7019
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "fancytext-generator",
"version": "1.0.0",
"description": "Fancy Text Generator for Instagram, TikTok & WhatsApp 30+ Cool Fonts",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"build:css": "tailwindcss -c tailwind.config.mjs -i ./styles/tailwind.input.css -o ./styles/tailwind.build.css",
"watch:css": "tailwindcss -c tailwind.config.mjs -i ./styles/tailwind.input.css -o ./styles/tailwind.build.css --watch",
"lint": "next lint",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\""
},
"dependencies": {
"clipboard": "^2.0.11",
"flowbite": "^2.3.0",
"flowbite-react": "^0.12.5",
"framer-motion": "^10.16.0",
"html2canvas": "^1.4.1",
"lucide-react": "^0.292.0",
"next": "^14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"webfontloader": "^1.6.28"
},
"devDependencies": {
"autoprefixer": "^10.4.21",
"eslint": "^8.57.0",
"eslint-config-next": "^14.0.4",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.17"
}
}

17
pages/_app.js Normal file
View File

@ -0,0 +1,17 @@
// pages/_app.jsx
import "@/styles/tailwind.build.css"; // dein TailwindBuild
import { fonts } from "@/lib/fonts";
// Alle CSSVariablen aus deinen next/fontLoaders, damit die Utilities greifen
const allFontVars = Object.values(fonts)
.map((f) => f.variable)
.join(" ");
export default function MyApp({ Component, pageProps }) {
return (
<main className={allFontVars}>
<Component {...pageProps} />
</main>
);
}

30
pages/_document.js Normal file
View File

@ -0,0 +1,30 @@
// pages/_document.jsx
import Document, { Html, Head, Main, NextScript } from "next/document";
export default class MyDocument extends Document {
render() {
return (
<Html lang="de">
<Head>
{/* Preconnects */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
{/* Alle 30 GoogleFonts */}
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Roboto:wght@100..900&family=Open+Sans&family=Montserrat:wght@100..900&family=Raleway:wght@100..900&family=Poppins:wght@100..900&family=Manrope:wght@100..700&family=Dancing+Script&family=Pacifico&family=Caveat&family=Indie+Flower&family=Great+Vibes&family=Sacramento&family=Alex+Brush&family=Anton&family=Bebas+Neue&family=Oswald:wght@200..700&family=Bangers&family=Abril+Fatface&family=Fredoka+One&family=Luckiest+Guy&family=Orbitron&family=Audiowide&family=Exo+2&family=Rajdhani&family=Syncopate&family=Press+Start+2P&family=Share+Tech+Mono&family=Playfair+Display&family=Cinzel&family=Italiana&family=Young+Serif&family=Caprasimo&family=Righteous&family=Luxurious+Roman&family=VT323&family=Neonderthaw&display=swap"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

32
pages/demo.jsx Normal file
View File

@ -0,0 +1,32 @@
// pages/demo.jsx
import React from "react";
import FontCard from "@/components/ui/FontCard"; // DefaultImport
import { fonts } from "@/lib/fonts";
export default function Demo() {
// Absicherung gegen undefined beim SSR
const robotoClass = fonts.roboto?.className || "";
return (
<section className="space-y-8 p-8">
{/* 1. next/font (voll integriert) */}
<h2 className={`${robotoClass} text-4xl`}>
Roboto aus next/font
</h2>
{/* 2. TailwindUtility aus Map */}
<p className="font-montserrat text-xl">
Dieselbe Font via <code>font-montserrat</code>
</p>
{/* 3. Deine PreviewKomponente */}
<FontCard
fontName="Pacifico"
transformedText="Hallo Instagram!"
category="handwriting"
isPopular
index={0}
/>
</section>
);
}

261
pages/index.jsx Normal file
View File

@ -0,0 +1,261 @@
// pages/index.jsx
import React, { useState, useEffect, useMemo, useCallback } from "react";
import Head from "next/head";
import { motion, AnimatePresence } from "framer-motion";
import {
fontTransforms,
getFontsByCategory,
getPopularFonts,
transformText,
} from "@/components/fontTransforms";
import MobileOptimizedHeader from "@/components/MobileOptimizedHeader";
import EnhancedTextInput from "@/components/EnhancedTextInput";
import ImprovedCategoryFilter from "@/components/ImprovedCategoryFilter";
import PerformanceOptimizedFontCard from "@/components/PerformanceOptimizedFontCard";
import InfoSection from "@/components/InfoSection";
import SocialButtons from "@/components/SocialButtons";
import SEOHead from "@/components/SEOHead";
if (typeof window !== "undefined") {
sessionStorage.removeItem("fancytext_recent_fonts");
}
export default function HomePage() {
const [inputText, setInputText] = useState("Hello Instagram!");
const [previewFont, setPreviewFont] = useState(null);
const [selectedCategory, setSelectedCategory] = useState("all");
const [searchQuery, setSearchQuery] = useState("");
const [recentFonts, setRecentFonts] = useState([]);
const [isMobile, setIsMobile] = useState(false);
const [animationsEnabled, setAnimationsEnabled] = useState(() => {
if (typeof window !== "undefined") {
const hasReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const isLowEndDevice = (navigator.hardwareConcurrency ?? 8) < 4;
return !hasReducedMotion && !isLowEndDevice;
}
return true;
});
useEffect(() => {
const checkMobile = () => {
if (typeof window !== "undefined") {
setIsMobile(window.innerWidth < 768 || /iPhone|iPad|iPod|Android/i.test(navigator.userAgent));
}
};
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
useEffect(() => {
setRecentFonts([]);
}, []);
const [debouncedText, setDebouncedText] = useState(inputText);
useEffect(() => {
const delay = isMobile ? 200 : 100;
const handler = setTimeout(() => setDebouncedText(inputText), delay);
return () => clearTimeout(handler);
}, [inputText, isMobile]);
const filteredFonts = useMemo(() => {
const list = getFontsByCategory(selectedCategory);
if (!searchQuery) return list;
const q = searchQuery.toLowerCase();
return list.filter((font) => {
const desc = fontTransforms[font]?.description?.toLowerCase() ?? "";
return font.toLowerCase().includes(q) || desc.includes(q);
});
}, [selectedCategory, searchQuery]);
const popularFonts = useMemo(() => getPopularFonts(), []);
const fontCounts = useMemo(() => {
const total = Object.keys(fontTransforms).length;
const counts = { all: total };
Object.values(fontTransforms).forEach(({ category }) => {
counts[category] = (counts[category] || 0) + 1;
});
return counts;
}, []);
const trackFontCopy = useCallback((fontName, text) => {
window.gtag?.("event", "font_copied", {
font_name: fontName,
text_length: text.length,
category: fontTransforms[fontName]?.category,
});
setRecentFonts((prev) => {
const updated = [fontName, ...prev.filter((f) => f !== fontName)].slice(0, 5);
return updated;
});
}, []);
const trackFontLike = useCallback((fontName, liked) => {
window.gtag?.("event", "font_liked", {
font_name: fontName,
action: liked ? "like" : "unlike",
});
}, []);
const handleQuickShare = useCallback(async () => {
const shareData = {
title: "FancyText - Cool Fonts! 🔥",
text: "Check out this app for cool Instagram & TikTok fonts! 30+ fonts free ✨",
url: window.location.href,
};
if (navigator.share) {
try {
await navigator.share(shareData);
} catch {}
} else {
await navigator.clipboard.writeText(`${shareData.text}\n${shareData.url}`);
alert("Link copied to clipboard! 🗌");
}
window.gtag?.("event", "app_shared", { method: "button_click" });
}, []);
const handleTextChange = useCallback((text) => {
setInputText(text);
setPreviewFont(null);
}, []);
const handleCategoryChange = useCallback((cat) => setSelectedCategory(cat), []);
const handleSearch = useCallback((q) => setSearchQuery(q), []);
const handleRandomFont = useCallback(() => {
const fontList = Object.keys(fontTransforms);
let tries = 0;
let newFont;
do {
newFont = fontList[Math.floor(Math.random() * fontList.length)];
tries++;
} while (newFont === previewFont && tries < 50);
setPreviewFont(newFont);
}, [previewFont]);
const displayText = previewFont
? transformText(inputText || "Try me!", previewFont)
: inputText;
return (
<>
<Head>
<title>FancyText | Viral Fonts</title>
<meta name="description" content="Make your posts pop with 30+ copy-paste fonts. Free, no login, mobile-ready. Works on IG, TikTok, Threads & more." />
<link rel="canonical" href="https://fancytext.app" />
<meta property="og:title" content="30+ Fancy Fonts for TikTok & Instagram 🔥" />
<meta property="og:description" content="Create viral bios, comments & posts in seconds no login, always free." />
<meta property="og:image" content="https://fancytext.app/social-preview.png" />
<meta property="og:url" content="https://fancytext.app" />
<meta property="og:type" content="website" />
<link rel="icon" href="/images/favicon.ico" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<script type="application/ld+json" dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "WebApplication",
name: "FancyText",
url: "https://fancytext.app",
applicationCategory: "WebApp",
operatingSystem: "All",
offers: {
"@type": "Offer",
price: "0.00",
priceCurrency: "USD",
},
})
}} />
</Head>
<div className="fixed top-4 right-4 z-[100] flex gap-4 text-sm text-black bg-white/90 px-3 py-1 rounded-lg shadow-lg backdrop-blur-sm">
<a href="#about" className="hover:underline">About</a>
<a href="#" onClick={(e) => {
e.preventDefault();
document.getElementById("privacy")?.scrollIntoView({ behavior: "smooth" });
}} className="hover:underline">Privacy</a>
</div>
<div className="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800 relative overflow-hidden">
<SEOHead currentText={inputText} />
<div className="relative z-10 container mx-auto px-4 py-8 max-w-6xl">
<MobileOptimizedHeader
animationsEnabled={animationsEnabled}
onToggleAnimations={setAnimationsEnabled}
totalFonts={Object.keys(fontTransforms).length}
onQuickShare={handleQuickShare}
/>
<EnhancedTextInput
inputText={displayText}
onTextChange={handleTextChange}
onSearch={handleSearch}
searchQuery={searchQuery}
placeholder="✍️ Start typing ..."
onRandomFont={handleRandomFont}
/>
<ImprovedCategoryFilter
selectedCategory={selectedCategory}
onCategoryChange={handleCategoryChange}
fontCounts={fontCounts}
isMobile={isMobile}
/>
{recentFonts.length > 0 && (
<div className="mb-16">
<h2 className="text-white text-lg font-semibold mb-4">🕘 Recently Used Fonts</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-10">
{recentFonts.map((name) => (
<PerformanceOptimizedFontCard
key={`recent_${name}`}
fontName={name}
transformedText={transformText(debouncedText, name)}
category={fontTransforms[name]?.category}
isPopular={popularFonts.includes(name)}
animationsEnabled={animationsEnabled}
index={-1}
onCopy={trackFontCopy}
onLike={trackFontLike}
onShare={handleQuickShare}
/>
))}
</div>
</div>
)}
<motion.div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-10 mb-8" layout>
<AnimatePresence mode="popLayout">
{filteredFonts.map((name, i) => (
<PerformanceOptimizedFontCard
key={name}
fontName={name}
transformedText={transformText(debouncedText, name)}
category={fontTransforms[name]?.category}
isPopular={popularFonts.includes(name)}
animationsEnabled={animationsEnabled}
index={i}
onCopy={trackFontCopy}
onLike={trackFontLike}
onShare={handleQuickShare}
/>
))}
</AnimatePresence>
</motion.div>
<SocialButtons onShare={handleQuickShare} />
<InfoSection currentText={debouncedText} />
</div>
</div>
</>
);
}
export async function getServerSideProps() {
return { props: {} };
}

5
postcss.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}, // genügt, weil tailwind hier ja schon fertig gebaut ist
},
};

2
postcss.config.mjs Normal file
View File

@ -0,0 +1,2 @@
import autoprefixer from "autoprefixer";
export default { plugins: [autoprefixer()] };

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

BIN
public/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

74
styles/globals.css Normal file
View File

@ -0,0 +1,74 @@
@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Pacifico&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Caveat&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Playfair+Display&display=swap");
@import url("https://fonts.googleapis.com/css2?family=VT323&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.font-montserrat {
font-family: "Montserrat", sans-serif;
}
.font-bebasneue {
font-family: "Bebas Neue", cursive;
}
.font-pacifico {
font-family: "Pacifico", cursive;
}
.font-caveat {
font-family: "Caveat", cursive;
}
.font-fredokaone {
font-family: "Fredoka One", cursive;
}
.font-playfair {
font-family: "Playfair Display", serif;
}
.font-vt323 {
font-family: "VT323", monospace;
}
/* Pseudo / Unicode Platzhalter */
.font-bubble {
font-family: inherit;
}
.font-glitch {
font-family: inherit;
}
.font-wide {
font-family: inherit;
}
.font-upsidedown {
font-family: inherit;
}
.font-strike {
text-decoration: line-through;
}
.font-underline {
text-decoration: underline;
}
}
/* eigene ZusatzUtilities */
@layer utilities {
.font-chilanka {
font-family: "Chilanka", cursive;
}
}
@layer components {
.text-shadow-lg {
text-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
}
/* SchnellCheck */
body {
background: #111;
color: #fff;
}

1964
styles/tailwind.build.css Normal file

File diff suppressed because it is too large Load Diff

29
tailwind.config.js Normal file
View File

@ -0,0 +1,29 @@
// tailwind.config.js
import { fonts } from "./lib/fonts.js";
const fontFamily = Object.fromEntries(
Object.entries(fonts)
.filter(([, d]) => d?.variable)
.map(([name, d]) => [
name.toLowerCase().replace(/\s+/g, ""),
[`var(${d.variable})`],
])
);
export default {
content: [
"./pages/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
"./lib/**/*.{js,jsx,ts,tsx}",
"./styles/**/*.{css,js}",
],
theme: { extend: { fontFamily } },
plugins: [],
// <<< Hier kommt die Safelist >>>
safelist: [
{
pattern: /^font-/,
variants: ["sm", "md", "lg", "xl"], // optional, falls responsive Klassen gebraucht werden
},
],
};

22
tailwind.config.mjs Normal file
View File

@ -0,0 +1,22 @@
import tailwindFonts from "./lib/tailwind-font-map.js"; // ← **ASCIIMinus!**
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./pages/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
"./entities/**/*.{js,jsx,ts,tsx}",
".flowbite-react/class-list.json",
],
theme: {
extend: {
fontFamily: Object.fromEntries(
Object.entries(tailwindFonts).map(([k, cssVar]) => [
k,
[`var(${cssVar})`],
])
),
},
},
plugins: [],
};