feat: add clerk auth support with convex (#548)

This commit is contained in:
Aman Varshney
2025-08-29 00:21:08 +05:30
committed by GitHub
parent 8d48ae0359
commit 54bcdf1cbc
153 changed files with 1954 additions and 771 deletions

View File

@@ -1,3 +1,4 @@
"use client";
import { Check, ChevronDown, ChevronRight, Copy, Terminal } from "lucide-react";
import Link from "next/link";
import { useState } from "react";

View File

@@ -1,5 +1,6 @@
import { api } from "@better-t-stack/backend/convex/_generated/api";
import { useQueryWithStatus } from "@better-t-stack/backend/convex/hooks";
"use client";
import type { api } from "@better-t-stack/backend/convex/_generated/api";
import { type Preloaded, usePreloadedQuery } from "convex/react";
import {
ChevronDown,
ChevronUp,
@@ -25,77 +26,17 @@ import {
sortSponsors,
} from "@/lib/sponsor-utils";
export default function SponsorsSection() {
export default function SponsorsSection({
preloadedSponsors,
}: {
preloadedSponsors: Preloaded<typeof api.sponsors.getSponsors>;
}) {
const sponsorsData = usePreloadedQuery(preloadedSponsors);
const [showPastSponsors, setShowPastSponsors] = useState(false);
const sponsorsQuery = useQueryWithStatus(api.sponsors.getSponsors);
if (sponsorsQuery.isPending) {
return (
<div className="mb-12">
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
<div className="flex items-center gap-2">
<Terminal className="h-5 w-5 text-primary" />
<span className="font-bold text-lg sm:text-xl">
SPONSORS_DATABASE.JSON
</span>
</div>
<div className="hidden h-px flex-1 bg-border sm:block" />
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">
[LOADING... RECORDS]
</span>
</div>
</div>
<div className="rounded border border-border p-8">
<div className="flex items-center justify-center gap-2">
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
<span className="text-muted-foreground">LOADING_SPONSORS.SH</span>
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
</div>
</div>
</div>
);
}
if (sponsorsQuery.isError) {
return (
<div className="mb-12">
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
<div className="flex items-center gap-2">
<Terminal className="h-5 w-5 text-primary" />
<span className="font-bold text-lg sm:text-xl">
SPONSORS_DATABASE.JSON
</span>
</div>
<div className="hidden h-px flex-1 bg-border sm:block" />
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">
[ERROR RECORDS]
</span>
</div>
</div>
<div className="rounded border border-border p-8">
<div className="text-center">
<div className="mb-4 flex items-center justify-center gap-2">
<span className="text-destructive">
ERROR_LOADING_SPONSORS.NULL
</span>
</div>
<div className="flex items-center justify-center gap-2 text-sm">
<span className="text-primary">$</span>
<span className="text-muted-foreground">
Please try again later!
</span>
</div>
</div>
</div>
</div>
);
}
const sponsors =
sponsorsQuery.data?.map((sponsor) => ({
sponsorsData.map((sponsor) => ({
...sponsor,
sponsor: {
...sponsor.sponsor,
@@ -106,7 +47,7 @@ export default function SponsorsSection() {
const visibleSponsors = filterVisibleSponsors(sponsors);
const sortedSponsors = sortSponsors(visibleSponsors);
const currentSponsors = filterCurrentSponsors(sortedSponsors);
const pastSponsors = filterPastSponsors(sortedSponsors);
const pastSponsors = filterPastSponsors(sortSponsors(sponsors));
const specialSponsors = sortSpecialSponsors(
filterSpecialSponsors(currentSponsors),
);

View File

@@ -157,7 +157,8 @@ function TechIcon({
theme === "light" &&
(icon.includes("drizzle") ||
icon.includes("prisma") ||
icon.includes("express"))
icon.includes("express") ||
icon.includes("clerk"))
) {
iconSrc = icon.replace(".svg", "-light.svg");
}
@@ -205,11 +206,24 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
database: "none",
orm: "none",
api: "none",
auth: "false",
dbSetup: "none",
examples: ["todo"],
};
const hasClerkCompatibleFrontend =
nextStack.webFrontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
f,
),
) ||
nextStack.nativeFrontend.some((f) =>
["native-nativewind", "native-unistyles"].includes(f),
);
if (nextStack.auth !== "clerk" || !hasClerkCompatibleFrontend) {
convexOverrides.auth = "none";
}
for (const [key, value] of Object.entries(convexOverrides)) {
const catKey = key as keyof StackState;
if (JSON.stringify(nextStack[catKey]) !== JSON.stringify(value)) {
@@ -257,7 +271,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
}
} else if (isBackendNone) {
const noneOverrides: Partial<StackState> = {
auth: "false",
auth: "none",
database: "none",
orm: "none",
api: "none",
@@ -336,20 +350,20 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
message: "ORM set to 'None' (requires a database)",
});
}
if (nextStack.auth === "true") {
if (nextStack.auth !== "none" && nextStack.backend !== "convex") {
notes.database.notes.push(
"Database 'None' selected: Auth will be disabled.",
);
notes.auth.notes.push(
"Authentication requires a database. It will be disabled.",
"Authentication requires a database. It will be set to 'None'.",
);
notes.database.hasIssue = true;
notes.auth.hasIssue = true;
nextStack.auth = "false";
nextStack.auth = "none";
changed = true;
changes.push({
category: "database",
message: "Authentication disabled (requires a database)",
message: "Authentication set to 'None' (requires a database)",
});
}
if (nextStack.dbSetup !== "none") {
@@ -696,6 +710,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.runtime.hasIssue = true;
notes.dbSetup.hasIssue = true;
nextStack.dbSetup = "d1";
changed = true;
changes.push({
category: "runtime",
message:
@@ -725,6 +740,57 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
});
}
if (nextStack.auth === "clerk") {
const hasClerkCompatibleFrontend =
nextStack.webFrontend.some((f) =>
[
"tanstack-router",
"react-router",
"tanstack-start",
"next",
].includes(f),
) ||
nextStack.nativeFrontend.some((f) =>
["native-nativewind", "native-unistyles"].includes(f),
);
if (!hasClerkCompatibleFrontend) {
notes.auth.notes.push(
"Clerk auth is not compatible with the selected frontends. Auth will be set to 'None'.",
);
notes.webFrontend.notes.push(
"Selected frontends are not compatible with Clerk auth. Auth will be disabled.",
);
notes.auth.hasIssue = true;
notes.webFrontend.hasIssue = true;
nextStack.auth = "none";
changed = true;
changes.push({
category: "auth",
message:
"Auth set to 'None' (Clerk not compatible with selected frontends)",
});
}
}
if (nextStack.backend === "convex" && nextStack.auth === "better-auth") {
notes.auth.notes.push(
"Better-Auth is not compatible with Convex backend. Auth will be set to 'None'.",
);
notes.backend.notes.push(
"Convex backend only supports Clerk auth or no auth. Auth will be disabled.",
);
notes.auth.hasIssue = true;
notes.backend.hasIssue = true;
nextStack.auth = "none";
changed = true;
changes.push({
category: "auth",
message:
"Auth set to 'None' (Better-Auth not compatible with Convex)",
});
}
const incompatibleAddons: string[] = [];
const isPWACompat = hasPWACompatibleFrontend(nextStack.webFrontend);
const isTauriCompat = hasTauriCompatibleFrontend(nextStack.webFrontend);
@@ -1120,9 +1186,7 @@ const generateCommand = (stackState: StackState): string => {
flags.push(`--orm ${stackState.orm}`);
}
if (!checkDefault("auth", stackState.auth)) {
if (stackState.auth === "false" && DEFAULT_STACK.auth === "true") {
flags.push("--no-auth");
}
flags.push(`--auth ${stackState.auth}`);
}
if (!checkDefault("dbSetup", stackState.dbSetup)) {
flags.push(`--db-setup ${stackState.dbSetup}`);
@@ -1527,16 +1591,12 @@ const StackBuilder = () => {
update[catKey] = techId;
} else {
if (
(category === "git" ||
category === "install" ||
category === "auth") &&
(category === "git" || category === "install") &&
techId === "false"
) {
update[catKey] = "true";
} else if (
(category === "git" ||
category === "install" ||
category === "auth") &&
(category === "git" || category === "install") &&
techId === "true"
) {
update[catKey] = "false";

View File

@@ -1,3 +1,4 @@
"use client";
import { api } from "@better-t-stack/backend/convex/_generated/api";
import { useNpmDownloadCounter } from "@erquhart/convex-oss-stats/react";
import NumberFlow, { continuous } from "@number-flow/react";
@@ -12,33 +13,16 @@ import {
Users,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
export default function StatsSection() {
const [analyticsData, setAnalyticsData] = useState<{
export default function StatsSection({
analyticsData,
}: {
analyticsData: {
totalProjects: number;
avgProjectsPerDay: string;
lastUpdated: string | null;
} | null>(null);
useEffect(() => {
const fetchAnalytics = async () => {
try {
const response = await fetch(
"https://r2.amanv.dev/analytics-minimal.json",
);
if (response.ok) {
const data = await response.json();
setAnalyticsData(data);
}
} catch (error) {
console.error("Failed to fetch analytics data:", error);
}
};
fetchAnalytics();
}, []);
};
}) {
const githubRepo = useQuery(api.stats.getGithubRepo, {
name: "AmanVarshney01/create-better-t-stack",
});

View File

@@ -1,12 +1,11 @@
"use client";
import { api } from "@better-t-stack/backend/convex/_generated/api";
import { useQueryWithStatus } from "@better-t-stack/backend/convex/hooks";
import type { api } from "@better-t-stack/backend/convex/_generated/api";
import { type Preloaded, usePreloadedQuery } from "convex/react";
import { Play, Terminal } from "lucide-react";
import { motion } from "motion/react";
import Image from "next/image";
import { Suspense } from "react";
import { Tweet, TweetSkeleton, type TwitterComponents } from "react-tweet";
import { Tweet, type TwitterComponents } from "react-tweet";
export const components: TwitterComponents = {
AvatarImg: (props) => {
@@ -92,101 +91,27 @@ const TweetCard = ({ tweetId, index }: { tweetId: string; index: number }) => (
</div>
<div className="w-full min-w-0 overflow-hidden">
<div style={{ width: "100%", minWidth: 0, maxWidth: "100%" }}>
<Suspense fallback={<TweetSkeleton />}>
<Tweet id={tweetId} components={components} />
</Suspense>
{/* <Suspense fallback={<TweetSkeleton />}> */}
<Tweet id={tweetId} components={components} />
{/* </Suspense> */}
</div>
</div>
</div>
</motion.div>
);
export default function Testimonials() {
const videosQuery = useQueryWithStatus(api.testimonials.getVideos);
const tweetsQuery = useQueryWithStatus(api.testimonials.getTweets);
export default function Testimonials({
preloadedTestimonialsTweet,
preloadedTestimonialsVideos,
}: {
preloadedTestimonialsTweet: Preloaded<typeof api.testimonials.getTweets>;
preloadedTestimonialsVideos: Preloaded<typeof api.testimonials.getVideos>;
}) {
const videosData = usePreloadedQuery(preloadedTestimonialsVideos);
const tweetsData = usePreloadedQuery(preloadedTestimonialsTweet);
const videos = videosQuery.data || [];
const tweets = tweetsQuery.data || [];
if (videosQuery.isPending || tweetsQuery.isPending) {
return (
<div className="mb-12 w-full max-w-full overflow-hidden px-4">
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
<div className="flex items-center gap-2">
<Play className="h-5 w-5 text-primary" />
<span className="font-bold text-lg sm:text-xl">
VIDEO_TESTIMONIALS.LOG
</span>
</div>
<div className="hidden h-px flex-1 bg-border sm:block" />
<span className="w-full text-right text-muted-foreground text-xs sm:w-auto sm:text-left">
[LOADING... ENTRIES]
</span>
</div>
<div className="mb-6 rounded border border-border p-8">
<div className="flex items-center justify-center gap-2">
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
<span className="text-muted-foreground">LOADING_VIDEOS.SH</span>
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
</div>
</div>
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
<div className="flex items-center gap-2">
<Terminal className="h-5 w-5 text-primary" />
<span className="font-bold text-lg sm:text-xl">
DEVELOPER_TESTIMONIALS.LOG
</span>
</div>
<div className="hidden h-px flex-1 bg-border sm:block" />
<span className="w-full text-right text-muted-foreground text-xs sm:w-auto sm:text-left">
[LOADING... ENTRIES]
</span>
</div>
<div className="rounded border border-border p-8">
<div className="flex items-center justify-center gap-2">
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
<span className="text-muted-foreground">LOADING_TWEETS.SH</span>
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
</div>
</div>
</div>
);
}
if (videosQuery.isError || tweetsQuery.isError) {
return (
<div className="mb-12 w-full max-w-full overflow-hidden px-4">
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
<div className="flex items-center gap-2">
<Play className="h-5 w-5 text-primary" />
<span className="font-bold text-lg sm:text-xl">
VIDEO_TESTIMONIALS.LOG
</span>
</div>
<div className="hidden h-px flex-1 bg-border sm:block" />
<span className="w-full text-right text-muted-foreground text-xs sm:w-auto sm:text-left">
[ERROR ENTRIES]
</span>
</div>
<div className="rounded border border-border p-8">
<div className="text-center">
<div className="mb-4 flex items-center justify-center gap-2">
<span className="text-destructive">
ERROR_LOADING_TESTIMONIALS.NULL
</span>
</div>
<div className="flex items-center justify-center gap-2 text-sm">
<span className="text-primary">$</span>
<span className="text-muted-foreground">
Please try again later!
</span>
</div>
</div>
</div>
</div>
);
}
const videos = videosData || [];
const tweets = tweetsData || [];
const getResponsiveColumns = (numCols: number) => {
const columns: string[][] = Array(numCols)