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