initial commit
This commit is contained in:
commit
49bb62fc4e
|
|
@ -0,0 +1,5 @@
|
|||
.git
|
||||
.gitignore
|
||||
node_modules
|
||||
.next
|
||||
*.log
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
class-list.json
|
||||
pid
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
.next
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss"
|
||||
]
|
||||
}
|
||||
|
|
@ -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?}?"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
@ -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";
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
fancytext-app:
|
||||
build: .
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
// 1) Google‑Fonts 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) System‑Fonts
|
||||
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) Pseudo‑Fonts
|
||||
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;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Enthält **nur** die CSS‑Variablen‑Namen, 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
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Deine sonstigen Next‑Optionen, z.B.:
|
||||
// reactStrictMode: true,
|
||||
// images: { domains: [...] },
|
||||
// rewrites: async () => [ ... ],
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
|
@ -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);
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// pages/_app.jsx
|
||||
import "@/styles/tailwind.build.css"; // dein Tailwind‑Build
|
||||
|
||||
import { fonts } from "@/lib/fonts";
|
||||
|
||||
// Alle CSS‑Variablen aus deinen next/font‑Loaders, 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 Google‑Fonts */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// pages/demo.jsx
|
||||
import React from "react";
|
||||
import FontCard from "@/components/ui/FontCard"; // Default‑Import
|
||||
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. Tailwind‑Utility aus Map */}
|
||||
<p className="font-montserrat text-xl">
|
||||
Dieselbe Font via <code>font-montserrat</code>
|
||||
</p>
|
||||
|
||||
{/* 3. Deine Preview‑Komponente */}
|
||||
<FontCard
|
||||
fontName="Pacifico"
|
||||
transformedText="Hallo Instagram!"
|
||||
category="handwriting"
|
||||
isPopular
|
||||
index={0}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -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: {} };
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}, // genügt, weil tailwind hier ja schon fertig gebaut ist
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
import autoprefixer from "autoprefixer";
|
||||
export default { plugins: [autoprefixer()] };
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
|
|
@ -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 Zusatz‑Utilities */
|
||||
@layer utilities {
|
||||
.font-chilanka {
|
||||
font-family: "Chilanka", cursive;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.text-shadow-lg {
|
||||
text-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Schnell‑Check */
|
||||
body {
|
||||
background: #111;
|
||||
color: #fff;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import tailwindFonts from "./lib/tailwind-font-map.js"; // ← **ASCII‑Minus!**
|
||||
|
||||
/** @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: [],
|
||||
};
|
||||
Loading…
Reference in New Issue