diff --git a/.env.example b/.env.example
index 361a4e5b..e42caad4 100644
--- a/.env.example
+++ b/.env.example
@@ -24,4 +24,8 @@ GOOGLE_CLIENT_SECRET="YOUR_GOOGLE_CLIENT_SECRET"
# Get a Google API Key: https://aistudio.google.com/apikey
GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
# Get a Groq API Key: https://console.groq.com/keys
-GROQ_API_KEY="YOUR_GROQ_API_KEY"
\ No newline at end of file
+GROQ_API_KEY="YOUR_GROQ_API_KEY"
+
+###### GOOGLE FONTS ######
+# Get a Google Fonts API Key: https://developers.google.com/fonts/docs/developer_api
+GOOGLE_FONTS_API_KEY="YOUR_GOOGLE_FONTS_API_KEY"
\ No newline at end of file
diff --git a/app/ai/components/ai-announcement.tsx b/app/ai/components/ai-announcement.tsx
index a44c3247..feaec178 100644
--- a/app/ai/components/ai-announcement.tsx
+++ b/app/ai/components/ai-announcement.tsx
@@ -1,18 +1,25 @@
+"use client";
+
+import { useSubscription } from "@/hooks/use-subscription";
import { ArrowRight } from "lucide-react";
import Link from "next/link";
export function AIAnnouncement() {
+ const { subscriptionStatus, isPending } = useSubscription();
+ const isPro = subscriptionStatus?.isSubscribed ?? false;
+
+ if (isPending || isPro) {
+ return null;
+ }
+
return (
-
- Beta
-
- Try the new AI Theme Editor
+ Upgrade to Pro for unlimited requests
diff --git a/app/ai/page.tsx b/app/ai/page.tsx
index 30a35446..9ed0545c 100644
--- a/app/ai/page.tsx
+++ b/app/ai/page.tsx
@@ -3,11 +3,11 @@ import { AIAnnouncement } from "./components/ai-announcement";
import { AIChatHero } from "./components/ai-chat-hero";
export const metadata: Metadata = {
- title: "AI Theme Editor for shadcn/ui — tweakcn",
+ title: "Image to shadcn/ui theme. Generate with AI — tweakcn",
description:
- "Effortlessly customize and generate shadcn/ui themes using tweakcn's AI-powered editor. Describe your desired theme, and let AI bring it to life. Supports Tailwind CSS, custom styles, and real-time previews.",
+ "Transform images into stunning shadcn/ui themes instantly with tweakcn's AI theme generator. Upload any image or describe your vision—our AI creates custom Tailwind CSS themes with real-time preview. Perfect for developers who want beautiful, production-ready themes in seconds.",
keywords:
- "ai theme editor, shadcn/ui, tailwind css, theme generator, ai design, ui customization, tweakcn, AI assisted theming, frontend development, web design AI",
+ "ai theme generator, image to theme, shadcn/ui themes, tailwind css generator, ai design tool, theme from image, ui customization, tweakcn, visual theme creator, color palette generator, design system ai, frontend theming, web design automation",
robots: "index, follow",
};
diff --git a/app/api/generate-theme/route.ts b/app/api/generate-theme/route.ts
index bd8b4549..842c0209 100644
--- a/app/api/generate-theme/route.ts
+++ b/app/api/generate-theme/route.ts
@@ -4,7 +4,7 @@ import { getCurrentUserId, logError } from "@/lib/shared";
import { validateSubscriptionAndUsage } from "@/lib/subscription";
import { SubscriptionRequiredError } from "@/types/errors";
import { requestSchema, responseSchema, SYSTEM_PROMPT } from "@/utils/ai/generate-theme";
-import { createGoogleGenerativeAI } from "@ai-sdk/google";
+import { createGoogleGenerativeAI, GoogleGenerativeAIProviderOptions } from "@ai-sdk/google";
import { Ratelimit } from "@upstash/ratelimit";
import { kv } from "@vercel/kv";
import { generateText, Output } from "ai";
@@ -61,6 +61,13 @@ export async function POST(req: NextRequest) {
system: SYSTEM_PROMPT,
messages,
abortSignal: req.signal,
+ providerOptions: {
+ google: {
+ thinkingConfig: {
+ thinkingBudget: 128,
+ },
+ } satisfies GoogleGenerativeAIProviderOptions,
+ },
});
if (usage) {
diff --git a/app/api/google-fonts/route.ts b/app/api/google-fonts/route.ts
new file mode 100644
index 00000000..ce110321
--- /dev/null
+++ b/app/api/google-fonts/route.ts
@@ -0,0 +1,54 @@
+import { PaginatedFontsResponse } from "@/types/fonts";
+import { FALLBACK_FONTS } from "@/utils/fonts";
+import { fetchGoogleFonts } from "@/utils/fonts/google-fonts";
+import { unstable_cache } from "next/cache";
+import { NextRequest, NextResponse } from "next/server";
+
+const cachedFetchGoogleFonts = unstable_cache(fetchGoogleFonts, ["google-fonts-catalogue"], {
+ tags: ["google-fonts-catalogue"],
+});
+
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const query = searchParams.get("q")?.toLowerCase() || "";
+ const category = searchParams.get("category")?.toLowerCase();
+ const limit = Math.min(Number(searchParams.get("limit")) || 50, 100);
+ const offset = Number(searchParams.get("offset")) || 0;
+
+ let googleFonts = FALLBACK_FONTS;
+
+ try {
+ googleFonts = await cachedFetchGoogleFonts(process.env.GOOGLE_FONTS_API_KEY);
+ } catch (error) {
+ console.error("Error fetching Google Fonts:", error);
+ console.log("Using fallback fonts");
+ }
+
+ // Filter fonts based on search query and category
+ let filteredFonts = googleFonts;
+
+ if (query) {
+ filteredFonts = filteredFonts.filter((font) => font.family.toLowerCase().includes(query));
+ }
+
+ if (category && category !== "all") {
+ filteredFonts = filteredFonts.filter((font) => font.category === category);
+ }
+
+ const paginatedFonts = filteredFonts.slice(offset, offset + limit);
+
+ const response: PaginatedFontsResponse = {
+ fonts: paginatedFonts,
+ total: filteredFonts.length,
+ offset,
+ limit,
+ hasMore: offset + limit < filteredFonts.length,
+ };
+
+ return NextResponse.json(response);
+ } catch (error) {
+ console.error("Error in Google Fonts API:", error);
+ return NextResponse.json({ error: "Failed to fetch fonts" }, { status: 500 });
+ }
+}
diff --git a/app/globals.css b/app/globals.css
index 9cbc8591..1744af5a 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -63,6 +63,7 @@
}
* {
+ color-scheme: light dark;
border-color: var(--color-border);
}
diff --git a/app/layout.tsx b/app/layout.tsx
index e1cee7c9..ef1ad163 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,4 +1,5 @@
import { AuthDialogWrapper } from "@/components/auth-dialog-wrapper";
+import { DynamicFontLoader } from "@/components/dynamic-font-loader";
import { GetProDialogWrapper } from "@/components/get-pro-dialog-wrapper";
import { PostHogInit } from "@/components/posthog-init";
import { ThemeProvider } from "@/components/theme-provider";
@@ -54,6 +55,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
+
@@ -65,6 +67,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
sizes="180x180"
/>
+
+ {/* PRELOAD FONTS USED BY BUILT-IN THEMES */}
-
404
-
Page not found
+
+
+
+
+
+
+
+
Toggle theme
+
+
+
+
+
+ 404
+
+
Oops, Lost in Space?
+
+ Go home or try switching the theme!
+
+
+
+ Back to Home
+
+
+
+
+
);
}
diff --git a/app/page.tsx b/app/page.tsx
index 0a704be8..2e2179db 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -9,6 +9,7 @@ import { Header } from "@/components/home/header";
import { Hero } from "@/components/home/hero";
import { HowItWorks } from "@/components/home/how-it-works";
import { Roadmap } from "@/components/home/roadmap";
+import { ThemeHotKeyHandler } from "@/components/home/theme-hotkey-handler";
import { ThemePresetSelector } from "@/components/home/theme-preset-selector";
import { useEffect, useState } from "react";
@@ -18,7 +19,7 @@ export default function Home() {
useEffect(() => {
const handleScroll = () => {
- if (window.scrollY > 10) {
+ if (window.scrollY > 10){
setIsScrolled(true);
} else {
setIsScrolled(false);
@@ -31,6 +32,7 @@ export default function Home() {
return (
+
+
);
}
diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx
index 78653ec6..62f0b7aa 100644
--- a/app/pricing/page.tsx
+++ b/app/pricing/page.tsx
@@ -1,5 +1,3 @@
-"use client";
-
import { NoiseEffect } from "@/components/effects/noise-effect";
import {
Accordion,
@@ -15,6 +13,13 @@ import { FREE_SUB_FEATURES, PRO_SUB_FEATURES } from "@/utils/subscription";
import { Calendar, Check, Circle, Mail } from "lucide-react";
import Link from "next/link";
import { CheckoutButton } from "./components/checkout-button";
+import { Metadata } from "next";
+import { Testimonials } from "@/components/home/testimonials";
+
+export const metadata: Metadata = {
+ title: "Pricing — tweakcn",
+ robots: "index, follow",
+};
export default function PricingPage() {
return (
@@ -131,6 +136,10 @@ export default function PricingPage() {
+