mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat: add clerk auth support with convex (#548)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { Check, ChevronDown, ChevronRight, Copy, Terminal } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user