Function bodies 143 total
getCached function · typescript · L28-L35 (8 LOC)app/api/trends/route.ts
function getCached<T>(key: string): T | null {
const entry = cache.get(key);
if (entry && entry.expiresAt > Date.now()) {
return entry.data as T;
}
cache.delete(key);
return null;
}setCache function · typescript · L37-L39 (3 LOC)app/api/trends/route.ts
function setCache(key: string, data: unknown): void {
cache.set(key, { data, expiresAt: Date.now() + CACHE_TTL });
}fetchWithTimeout function · typescript · L42-L55 (14 LOC)app/api/trends/route.ts
async function fetchWithTimeout(url: string, options: RequestInit = {}): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
return response;
} finally {
clearTimeout(timeoutId);
}
}GET function · typescript · L68-L163 (96 LOC)app/api/trends/route.ts
export async function GET(request: NextRequest) {
try {
// Rate limiting
const clientIp = getClientIp(request.headers);
const rateLimit = withRateLimit(clientIp, "trends-get", RATE_LIMIT_CONFIGS.api);
if (!rateLimit.allowed) {
return NextResponse.json(
{ success: false, error: "Too many requests", code: "RATE_LIMITED" },
{ status: 429, headers: rateLimit.headers }
);
}
// Authentication check
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: "Please login to access trends" },
{ status: 401 }
);
}
// Validate input
const { searchParams } = new URL(request.url);
const validation = GetTrendsSchema.safeParse({
country: searchParams.get("country") || "US",
category: searchParams.get("category") || "all",
});
if (!validation.success) {
return NextResponse.json(
{ success: false, error: validatifilterByCategory function · typescript · L166-L183 (18 LOC)app/api/trends/route.ts
function filterByCategory(trends: TrendResult[], category: string): TrendResult[] {
const categoryKeywords: Record<string, string[]> = {
entertainment: ["movie", "film", "show", "music", "actor", "singer", "celebrity", "netflix", "disney", "hulu", "tv", "series", "album", "concert"],
sports: ["game", "nfl", "nba", "mlb", "soccer", "football", "basketball", "baseball", "team", "player", "championship", "super bowl", "match", "score"],
business: ["stock", "market", "company", "ceo", "business", "economy", "trade", "investment", "earnings", "ipo", "merger"],
technology: ["ai", "tech", "apple", "google", "microsoft", "software", "app", "phone", "computer", "robot", "crypto", "bitcoin"],
health: ["health", "covid", "vaccine", "doctor", "hospital", "disease", "medical", "treatment", "drug", "fda"],
science: ["science", "space", "nasa", "research", "study", "discovery", "climate", "planet", "species"],
};
const keywords = categoryKeywords[category] || [];
if (fetchGoogleTrendsRSS function · typescript · L186-L232 (47 LOC)app/api/trends/route.ts
async function fetchGoogleTrendsRSS(country: string): Promise<TrendResult[]> {
try {
const response = await fetchWithTimeout(`https://trends.google.com/trending/rss?geo=${country}`, {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
},
cache: "no-store",
});
if (!response.ok) {
console.error("Google RSS status:", response.status);
return [];
}
const xml = await response.text();
const trends: TrendResult[] = [];
const items = xml.match(/<item>([\s\S]*?)<\/item>/g) || [];
for (const item of items.slice(0, 25)) {
const title = extractCDATA(item, "title") || extractTag(item, "title");
const traffic = extractTag(item, "ht:approx_traffic");
const picture = extractTag(item, "ht:picture");
const newsItems = item.match(/<ht:news_item>([\s\S]*?)<\/ht:news_item>/g) || [];
const articles = newsItems.slicfetchYouTubeTrending function · typescript · L235-L289 (55 LOC)app/api/trends/route.ts
async function fetchYouTubeTrending(country: string): Promise<TrendResult[]> {
try {
const response = await fetchWithTimeout(`https://www.youtube.com/feed/trending?gl=${country}`, {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
},
cache: "no-store",
});
if (!response.ok) {
console.error("YouTube trending status:", response.status);
return [];
}
const html = await response.text();
const trends: TrendResult[] = [];
// Extract video data from ytInitialData
const dataMatch = html.match(/ytInitialData\s*=\s*({[\s\S]*?});\s*<\/script>/);
if (dataMatch) {
try {
const data = JSON.parse(dataMatch[1]);
const tabs = data.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
for (const tab of tabs) {
const contents = tab.tabRenderer?.content?.Repobility · code-quality intelligence platform · https://repobility.com
parseViews function · typescript · L291-L295 (5 LOC)app/api/trends/route.ts
function parseViews(text: string): number {
const match = text.match(/([\d,.]+)/);
if (!match) return 0;
return parseInt(match[1].replace(/[,.]/g, ""), 10) || 0;
}fetchRedditTrends function · typescript · L298-L354 (57 LOC)app/api/trends/route.ts
async function fetchRedditTrends(category: string): Promise<TrendResult[]> {
try {
const subreddits: Record<string, string> = {
all: "popular",
entertainment: "entertainment+movies+television+Music",
sports: "sports+nfl+nba+soccer",
business: "business+stocks+wallstreetbets+finance",
technology: "technology+programming+gadgets",
health: "health+fitness+nutrition",
science: "science+space+environment",
};
const sub = subreddits[category] || "popular";
const response = await fetchWithTimeout(`https://www.reddit.com/r/${sub}/.rss?limit=20`, {
headers: {
"User-Agent": "ThreadSmith/1.0",
},
cache: "no-store",
});
if (!response.ok) {
console.error("Reddit RSS status:", response.status);
return [];
}
const xml = await response.text();
const trends: TrendResult[] = [];
// Parse Atom feed entries
const entries = xml.match(/<entry>[\s\S]*?<\/entry>/g) || [];
forfetchHackerNewsTrends function · typescript · L357-L394 (38 LOC)app/api/trends/route.ts
async function fetchHackerNewsTrends(): Promise<TrendResult[]> {
try {
const idsResponse = await fetchWithTimeout("https://hacker-news.firebaseio.com/v0/topstories.json", {
cache: "no-store",
});
if (!idsResponse.ok) return [];
const ids = await idsResponse.json();
const topIds = ids.slice(0, 15); // Reduced from 20 to limit parallel requests
const stories = await Promise.all(
topIds.map(async (id: number) => {
try {
const res = await fetchWithTimeout(`https://hacker-news.firebaseio.com/v0/item/${id}.json`);
return res.json();
} catch {
return null;
}
})
);
return stories
.filter((s): s is NonNullable<typeof s> => s !== null && s.title)
.map((story) => ({
title: story.title,
url: story.url || `https://news.ycombinator.com/item?id=${story.id}`,
points: story.score || 0,
author: story.by || "unknown",
comments: story.descendantsextractTag function · typescript · L396-L400 (5 LOC)app/api/trends/route.ts
function extractTag(xml: string, tag: string): string {
const regex = new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`);
const match = xml.match(regex);
return match ? match[1].trim() : "";
}extractCDATA function · typescript · L402-L406 (5 LOC)app/api/trends/route.ts
function extractCDATA(xml: string, tag: string): string {
const regex = new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]></${tag}>`);
const match = xml.match(regex);
return match ? match[1].trim() : "";
}POST function · typescript · L424-L505 (82 LOC)app/api/trends/route.ts
export async function POST(request: NextRequest) {
try {
// Rate limiting (stricter for keyword search)
const clientIp = getClientIp(request.headers);
const rateLimit = withRateLimit(clientIp, "trends-search", {
windowMs: 60 * 1000,
maxRequests: 10, // 10 searches per minute
});
if (!rateLimit.allowed) {
return NextResponse.json(
{ success: false, error: "Too many requests", code: "RATE_LIMITED" },
{ status: 429, headers: rateLimit.headers }
);
}
// Authentication check
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: "Please login to search trends" },
{ status: 401 }
);
}
// Validate and sanitize input
const body = await request.json();
const validation = PostTrendsSchema.safeParse(body);
if (!validation.success) {
return NextResponse.json(
{ success: false, error: validation.error.errofetchRelatedQueries function · typescript · L507-L532 (26 LOC)app/api/trends/route.ts
async function fetchRelatedQueries(keyword: string, country: string) {
try {
const googleTrends = await import("google-trends-api");
const results = await googleTrends.default.relatedQueries({
keyword,
geo: country,
});
const parsed = JSON.parse(results);
const defaultData = parsed.default;
return {
top: (defaultData?.rankedList?.[0]?.rankedKeyword || []).slice(0, 10).map((item: { query: string; value: number }) => ({
query: item.query,
value: item.value,
})),
rising: (defaultData?.rankedList?.[1]?.rankedKeyword || []).slice(0, 10).map((item: { query: string; formattedValue: string }) => ({
query: item.query,
growth: item.formattedValue,
})),
};
} catch (error) {
console.error("Related queries error:", error);
return { top: [], rising: [] };
}
}fetchInterestOverTime function · typescript · L534-L553 (20 LOC)app/api/trends/route.ts
async function fetchInterestOverTime(keyword: string, country: string) {
try {
const googleTrends = await import("google-trends-api");
const results = await googleTrends.default.interestOverTime({
keyword,
geo: country,
});
const parsed = JSON.parse(results);
const timeline = parsed.default?.timelineData || [];
return timeline.slice(-30).map((point: { formattedTime: string; value: number[] }) => ({
time: point.formattedTime,
value: point.value?.[0] || 0,
}));
} catch (error) {
console.error("Interest over time error:", error);
return [];
}
}All rows scored by the Repobility analyzer (https://repobility.com)
GET function · typescript · L6-L61 (56 LOC)app/api/users/history/route.ts
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: "Please login to continue" },
{ status: 401 }
);
}
await dbConnect();
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1");
const limit = parseInt(searchParams.get("limit") || "20");
const type = searchParams.get("type"); // "hooks" | "thread" | null
const skip = (page - 1) * limit;
// Build query
const query: Record<string, unknown> = { userId: session.user.id };
if (type) {
query.type = type;
}
// Get generations
const [generations, total] = await Promise.all([
Generation.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.lean(),
Generation.countDocuments(query),
]);
return NextResponse.json({
success: trLoginPage function · typescript · L11-L197 (187 LOC)app/(auth)/login/page.tsx
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setIsLoading(true);
const result = await signIn("credentials", {
email,
password,
redirect: false,
});
if (result?.error) {
setError("Invalid email or password");
setIsLoading(false);
} else {
router.push("/dashboard");
router.refresh();
}
};
const handleGoogleLogin = async () => {
setIsGoogleLoading(true);
await signIn("google", { callbackUrl: "/dashboard" });
};
return (
<div className="min-h-screen flex items-center justify-centRegisterPage function · typescript · L12-L231 (220 LOC)app/(auth)/register/page.tsx
export default function RegisterPage() {
const router = useRouter();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setIsLoading(true);
if (password.length < 6) {
setError("Password must be at least 6 characters");
setIsLoading(false);
return;
}
// First register the user
const result = await authApi.register({ email, password, name });
if (result.success) {
// Then sign in with credentials
const signInResult = await signIn("credentials", {
email,
password,
redirect: false,
});
if (signInResult?.errAchievementsPage function · typescript · L61-L290 (230 LOC)app/(dashboard)/dashboard/achievements/page.tsx
export default function AchievementsPage() {
const [achievements, setAchievements] = useState<AchievementData[]>([]);
const [streak, setStreak] = useState<StreakData | null>(null);
const [totalXP, setTotalXP] = useState(0);
const [unlockedCount, setUnlockedCount] = useState(0);
const [totalCount, setTotalCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedCategory, setSelectedCategory] = useState<string>("all");
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setIsLoading(true);
const [achievementsResult, streakResult] = await Promise.all([
achievementsApi.list(),
achievementsApi.getStreak(),
]);
if (achievementsResult.success && achievementsResult.data) {
const data = achievementsResult.data as {
achievements: AchievementData[];
totalXP: number;
unlockedCount: number;
totalCountHistoryPage function · typescript · L10-L199 (190 LOC)app/(dashboard)/dashboard/history/page.tsx
export default function HistoryPage() {
const [generations, setGenerations] = useState<IGeneration[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [filter, setFilter] = useState<"all" | "hooks" | "thread">("all");
const [copiedId, setCopiedId] = useState<string | null>(null);
useEffect(() => {
fetchHistory();
}, [page, filter]);
const fetchHistory = async () => {
setIsLoading(true);
const type = filter === "all" ? undefined : filter;
const result = await userApi.history(page, 10, type);
if (result.success && result.data) {
const data = result.data as {
generations: IGeneration[];
pagination: { totalPages: number };
};
setGenerations(data.generations);
setTotalPages(data.pagination.totalPages);
}
setIsLoading(false);
};
const handleCopy = async (text: string, id: string) => {
await navigatoSchedulerPage function · typescript · L53-L414 (362 LOC)app/(dashboard)/dashboard/scheduler/page.tsx
export default function SchedulerPage() {
const [posts, setPosts] = useState<ScheduledPostData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingPost, setEditingPost] = useState<ScheduledPostData | null>(null);
const [statusFilter, setStatusFilter] = useState<string>("");
// Form state
const [formHook, setFormHook] = useState("");
const [formThread, setFormThread] = useState("");
const [formPlatforms, setFormPlatforms] = useState<Platform[]>(["facebook"]);
const [formScheduledAt, setFormScheduledAt] = useState("");
const [formRecurrence, setFormRecurrence] = useState<"none" | "daily" | "weekly">("none");
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
fetchPosts();
}, [statusFilter]);
const fetchPosts = async () => {
setIsLoading(true);
const result = await schedulerApi.lisSettingsPage function · typescript · L9-L138 (130 LOC)app/(dashboard)/dashboard/settings/page.tsx
export default function SettingsPage() {
const { user } = useAuth();
if (!user) return null;
const limits = PLAN_LIMITS[user.plan];
return (
<div className="max-w-2xl mx-auto font-body">
<div className="mb-8">
<h1 className="text-2xl sm:text-3xl font-heading font-bold text-white flex items-center gap-3">
<Settings className="w-7 h-7 sm:w-8 sm:h-8 text-purple-400" />
Settings
</h1>
<p className="text-gray-400 text-sm sm:text-base font-accent mt-1">Manage your account</p>
</div>
<div className="space-y-6">
{/* Profile */}
<Card>
<h2 className="text-lg sm:text-xl font-heading font-semibold mb-4 flex items-center gap-2 text-white">
<User className="w-5 h-5 text-purple-400" />
Profile
</h2>
<div className="space-y-4">
<div>
<label className="text-sm text-gray-400">Name</label>
<p className="font-medium">{uTrendsPage function · typescript · L98-L354 (257 LOC)app/(dashboard)/dashboard/trends/page.tsx
export default function TrendsPage() {
const [country, setCountry] = useState("US");
const [category, setCategory] = useState("all");
const [selectedNiche, setSelectedNiche] = useState<string | null>(null);
const [trends, setTrends] = useState<TrendsData | null>(null);
const [keywordData, setKeywordData] = useState<KeywordData | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [searchKeyword, setSearchKeyword] = useState("");
const [isSearching, setIsSearching] = useState(false);
const [activeTab, setActiveTab] = useState<"google" | "youtube" | "reddit" | "hackernews" | "niche">("google");
useEffect(() => {
fetchTrends();
}, [country, category]);
const fetchTrends = async () => {
setIsLoading(true);
try {
const res = await fetch(`/api/trends?country=${country}&category=${category}`);
const data = await res.json();
if (data.success) {
setTrends(dWant fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
LoadingState function · typescript · L392-L404 (13 LOC)app/(dashboard)/dashboard/trends/page.tsx
function LoadingState({ country }: { country?: { flag: string; name: string } }) {
return (
<div className="flex flex-col items-center justify-center py-20">
<div className="relative w-16 h-16">
<div className="absolute inset-0 rounded-full border-4 border-green-500/20"></div>
<div className="absolute inset-0 rounded-full border-4 border-transparent border-t-green-500 animate-spin"></div>
<TrendingUp className="w-6 h-6 text-green-400 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
</div>
<p className="text-gray-400 mt-4">Fetching live trends...</p>
{country && <p className="text-gray-500 text-sm">{country.flag} {country.name}</p>}
</div>
);
}GoogleTrends function · typescript · L407-L457 (51 LOC)app/(dashboard)/dashboard/trends/page.tsx
function GoogleTrends({ trends, country }: { trends: TrendItem[]; country?: string }) {
if (trends.length === 0) {
return (
<Card className="p-8 text-center">
<TrendingUp className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-400">No Google trends found{country ? ` for ${country}` : ""}</p>
</Card>
);
}
return (
<div className="space-y-3">
{trends.map((trend, i) => (
<Card key={i} className="p-4 hover:border-blue-500/30 transition-all">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-blue-500/20 flex items-center justify-center shrink-0">
<span className="text-lg font-bold text-blue-400">#{i + 1}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="font-semibold text-white text-lg">{trend.title}<YouTubeTrends function · typescript · L460-L500 (41 LOC)app/(dashboard)/dashboard/trends/page.tsx
function YouTubeTrends({ trends }: { trends: TrendItem[] }) {
if (trends.length === 0) {
return (
<Card className="p-8 text-center">
<Play className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-400">No YouTube trends available</p>
</Card>
);
}
return (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{trends.map((trend, i) => (
<a key={i} href={trend.url} target="_blank" rel="noopener noreferrer" className="block">
<Card className="p-0 overflow-hidden hover:border-red-500/30 transition-all group">
<div className="relative">
{trend.image && (
<img src={trend.image} alt="" className="w-full h-36 object-cover" />
)}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-all flex items-center justify-center">
<Play className="w-12 h-12 text-white" />
</div>formatViews function · typescript · L502-L506 (5 LOC)app/(dashboard)/dashboard/trends/page.tsx
function formatViews(views: number): string {
if (views >= 1000000) return `${(views / 1000000).toFixed(1)}M`;
if (views >= 1000) return `${(views / 1000).toFixed(0)}K`;
return views.toString();
}RedditTrends function · typescript · L509-L550 (42 LOC)app/(dashboard)/dashboard/trends/page.tsx
function RedditTrends({ trends }: { trends: TrendItem[] }) {
if (trends.length === 0) {
return (
<Card className="p-8 text-center">
<Users className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-400">No Reddit trends available</p>
</Card>
);
}
return (
<div className="space-y-3">
{trends.map((trend, i) => (
<Card key={i} className="p-4 hover:border-orange-500/30 transition-all">
<div className="flex items-start gap-4">
<div className="flex flex-col items-center gap-1 shrink-0 w-12">
<ArrowUp className="w-5 h-5 text-orange-500" />
<span className="text-sm font-bold text-orange-500">
{trend.score ? (trend.score >= 1000 ? `${(trend.score / 1000).toFixed(1)}k` : trend.score) : 0}
</span>
</div>
<div className="flex-1 min-w-0">
<a href={trend.url} target="_blank" rel="noopener noreferrer" classNHackerNewsTrends function · typescript · L553-L589 (37 LOC)app/(dashboard)/dashboard/trends/page.tsx
function HackerNewsTrends({ trends }: { trends: TrendItem[] }) {
if (trends.length === 0) {
return (
<Card className="p-8 text-center">
<Code className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-400">No Hacker News trends available</p>
</Card>
);
}
return (
<div className="space-y-3">
{trends.map((trend, i) => (
<Card key={i} className="p-4 hover:border-amber-500/30 transition-all">
<div className="flex items-start gap-4">
<div className="flex flex-col items-center gap-1 shrink-0 w-12">
<Zap className="w-5 h-5 text-amber-500" />
<span className="text-sm font-bold text-amber-500">{trend.points}</span>
</div>
<div className="flex-1 min-w-0">
<a href={trend.url} target="_blank" rel="noopener noreferrer" className="font-medium text-white hover:text-amber-400 line-clamp-2">
{trend.title}
</a>DashboardLayout function · typescript · L41-L221 (181 LOC)app/(dashboard)/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const { user, isLoading, isAuthenticated, logout } = useAuth();
const [streak, setStreak] = useState<{ currentStreak: number; isActive: boolean } | null>(null);
const [sidebarOpen, setSidebarOpen] = useState(false);
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push("/login");
}
}, [isLoading, isAuthenticated, router]);
// Close sidebar on route change
useEffect(() => {
setSidebarOpen(false);
}, [pathname]);
// Fetch streak data
useEffect(() => {
if (isAuthenticated) {
achievementsApi.getStreak().then((result) => {
if (result.success && result.data) {
const data = result.data as { currentStreak: number; isActive: boolean };
setStreak(data);
}
});
}
}, [isAuthenticated]);
if (isLoading) {
return (
<div claRootLayout function · typescript · L56-L71 (16 LOC)app/layout.tsx
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={`${spaceGrotesk.variable} ${inter.variable} ${dmSans.variable} ${jetbrainsMono.variable}`}>
<body className="antialiased font-body">
<Providers>
<div className="animated-bg" />
{children}
</Providers>
</body>
</html>
);
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
NotFound function · typescript · L5-L31 (27 LOC)app/not-found.tsx
export default function NotFound() {
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="text-center">
<div className="text-8xl font-bold text-neon-gradient mb-4">404</div>
<h2 className="text-2xl font-bold mb-2">Page Not Found</h2>
<p className="text-gray-400 mb-8">
The page you're looking for doesn't exist or has been moved.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link href="/">
<Button>
<Home className="w-4 h-4 mr-2" />
Go Home
</Button>
</Link>
<Link href="/dashboard">
<Button variant="glass">
<Search className="w-4 h-4 mr-2" />
Dashboard
</Button>
</Link>
</div>
</div>
</div>
);
}generateMetadata function · typescript · L11-L61 (51 LOC)app/share/[id]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params;
try {
await dbConnect();
const sharedCard = await SharedCard.findOne({ uniqueId: id }).select("title description uniqueId");
if (!sharedCard) {
return {
title: "Card Not Found | ThreadN",
description: "This shared card could not be found.",
};
}
const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://threadn.launchory.org";
const imageUrl = `${appUrl}/api/share/${id}/image`;
const shareUrl = `${appUrl}/share/${id}`;
return {
title: `${sharedCard.title} | ThreadN`,
description: sharedCard.description || "Created with ThreadN - AI-powered content creation",
openGraph: {
title: sharedCard.title,
description: sharedCard.description || "Created with ThreadN - AI-powered content creation",
url: shareUrl,
siteName: "ThreadN",
images: [
{
SharePage function · typescript · L63-L66 (4 LOC)app/share/[id]/page.tsx
export default async function SharePage({ params }: Props) {
const { id } = await params;
return <SharePageClient shareId={id} />;
}SharePageClient function · typescript · L22-L361 (340 LOC)app/share/[id]/SharePageClient.tsx
export default function SharePageClient({ shareId }: SharePageClientProps) {
const [card, setCard] = useState<SharedCardData | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
useEffect(() => {
fetchCard();
}, [shareId]);
const fetchCard = async () => {
try {
const res = await fetch(`/api/share/${shareId}`);
const data = await res.json();
if (data.success) {
setCard(data.data.card);
} else {
setError(data.error || "Card not found");
}
} catch {
setError("Failed to load card");
} finally {
setIsLoading(false);
}
};
const handleDownload = async () => {
if (!card) return;
const link = document.createElement("a");
link.download = `${card.title.replace(/[^a-zA-Z0-9]/g, "_")}.png`;
link.href = card.imageData;
link.click();
};
const handleCopyLink = asyncTermsPage function · typescript · L17-L313 (297 LOC)app/terms/page.tsx
export default function TermsPage() {
return (
<div className="min-h-screen bg-gradient-to-b from-gray-950 via-gray-900 to-gray-950 font-body">
<Navbar showLinks={false} />
{/* Hero */}
<section className="py-12 sm:py-16 px-4 relative">
<div className="absolute top-10 left-1/4 w-64 h-64 bg-purple-500/10 rounded-full blur-3xl" />
<div className="max-w-4xl mx-auto text-center relative">
<div className="inline-flex items-center justify-center w-14 h-14 sm:w-16 sm:h-16 rounded-2xl bg-gradient-to-br from-purple-500/20 to-cyan-500/20 mb-5 sm:mb-6">
<FileText className="w-7 h-7 sm:w-8 sm:h-8 text-purple-400" />
</div>
<h1 className="text-3xl sm:text-4xl font-heading font-bold mb-3 sm:mb-4 text-white">Terms of Service</h1>
<p className="text-gray-400 text-sm sm:text-base font-accent mb-2">
Please read these terms carefully before using ThreadN
</p>
<p className="text-xs Footer function · typescript · L18-L277 (260 LOC)components/Footer.tsx
export function Footer({ showCTA = true }: FooterProps) {
return (
<footer className="relative pt-12 sm:pt-20 pb-8 sm:pb-10 px-4 overflow-hidden">
{/* Background Elements */}
<div className="absolute inset-0 bg-gradient-to-t from-black via-gray-950 to-transparent" />
<div className="absolute bottom-0 left-1/4 w-48 sm:w-96 h-48 sm:h-96 bg-purple-500/5 rounded-full blur-3xl" />
<div className="absolute bottom-0 right-1/4 w-48 sm:w-96 h-48 sm:h-96 bg-cyan-500/5 rounded-full blur-3xl" />
<div className="max-w-6xl mx-auto relative">
{/* Top Section - Newsletter/CTA */}
{showCTA && (
<div className="glass-card p-5 sm:p-8 mb-10 sm:mb-16 bg-gradient-to-r from-purple-500/10 via-transparent to-cyan-500/10">
<div className="flex flex-col md:flex-row items-center justify-between gap-4 sm:gap-6 text-center md:text-left">
<div>
<h3 className="text-lg sm:text-xl font-heading font-bold mb-1 sm:mb-2Navbar function · typescript · L14-L154 (141 LOC)components/Navbar.tsx
export function Navbar({ showLinks = true }: NavbarProps) {
const { data: session, status } = useSession();
const isLoggedIn = status === "authenticated" && session?.user;
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<nav className="navbar-glass sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link href="/" className="flex items-center">
<Image
src="/logo.png"
alt="ThreadN"
width={140}
height={40}
className="h-8 sm:h-10 w-auto"
priority
/>
</Link>
{/* Desktop Nav Links */}
{showLinks && (
<div className="hidden md:flex items-center gap-8">
<Link
href="/#features"
className="text-gray-400 hover:text-white transition-colors teProviders function · typescript · L6-L8 (3 LOC)components/Providers.tsx
export function Providers({ children }: { children: ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}Repobility · code-quality intelligence platform · https://repobility.com
Card function · typescript · L12-L24 (13 LOC)components/ui/Card.tsx
export function Card({ children, className, hover = false }: CardProps) {
return (
<div
className={cn(
"glass-card p-6",
hover && "hover-lift cursor-pointer",
className
)}
>
{children}
</div>
);
}CardHeader function · typescript · L26-L34 (9 LOC)components/ui/Card.tsx
export function CardHeader({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn("mb-4", className)}>{children}</div>;
}CardTitle function · typescript · L36-L48 (13 LOC)components/ui/Card.tsx
export function CardTitle({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<h3 className={cn("text-lg font-semibold text-white", className)}>
{children}
</h3>
);
}CardContent function · typescript · L50-L58 (9 LOC)components/ui/Card.tsx
export function CardContent({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return <div className={cn("text-gray-300", className)}>{children}</div>;
}Modal function · typescript · L15-L65 (51 LOC)components/ui/Modal.tsx
export function Modal({ isOpen, onClose, title, children, className }: ModalProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [isOpen]);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
if (isOpen) {
window.addEventListener("keydown", handleEscape);
}
return () => {
window.removeEventListener("keydown", handleEscape);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div
className={cn("modal-content", className)}
onClick={(e) => e.stopPropagation()}
>
{title && (
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-white">{title}<useAuth function · typescript · L9-L71 (63 LOC)hooks/useAuth.ts
export function useAuth() {
const { data: session, status, update } = useSession();
const router = useRouter();
const [user, setUser] = useState<IUserPublic | null>(null);
const [isLoadingUser, setIsLoadingUser] = useState(false);
const isLoading = status === "loading";
const isAuthenticated = status === "authenticated";
// Use session role directly for faster admin check
const isAdmin = session?.user?.role === "admin";
// Fetch full user data from database
const fetchUser = useCallback(async () => {
if (!session?.user?.id) return;
setIsLoadingUser(true);
try {
const response = await api("/auth/me");
if (response.success && response.data) {
setUser((response.data as { user: IUserPublic }).user);
}
} catch (error) {
console.error("Failed to fetch user:", error);
} finally {
setIsLoadingUser(false);
}
}, [session?.user?.id]);
// Fetch user data when authenticated
useEffect(() => {
if (status =checkAndUnlockAchievement function · typescript · L14-L44 (31 LOC)lib/achievements.ts
export async function checkAndUnlockAchievement(
userId: string,
achievementId: string
): Promise<AchievementUnlockResult> {
await dbConnect();
const achievement = ACHIEVEMENTS[achievementId];
if (!achievement) {
return { unlocked: false };
}
// Check if already unlocked
const existing = await UserAchievement.findOne({
userId,
achievementId,
});
if (existing) {
return { unlocked: true, achievement, isNew: false };
}
// Create new achievement
await UserAchievement.create({
userId,
achievementId,
unlockedAt: new Date(),
progress: achievement.requirement,
});
return { unlocked: true, achievement, isNew: true };
}checkAllAchievements function · typescript · L49-L109 (61 LOC)lib/achievements.ts
export async function checkAllAchievements(userId: string): Promise<AchievementUnlockResult[]> {
await dbConnect();
const results: AchievementUnlockResult[] = [];
// Get user stats
const user = await User.findById(userId);
if (!user) return results;
const streak = await UserStreak.findOne({ userId });
// Get counts
const [templateCount, collectionCount, scheduledCount] = await Promise.all([
Template.countDocuments({ userId }),
Collection.countDocuments({ userId }),
ScheduledPost.countDocuments({ userId }),
]);
// Check generation achievements
if (user.usage.totalHooks >= 1) {
results.push(await checkAndUnlockAchievement(userId, "first_hook"));
}
if (user.usage.totalHooks >= 10) {
results.push(await checkAndUnlockAchievement(userId, "hooks_10"));
}
if (user.usage.totalHooks >= 100) {
results.push(await checkAndUnlockAchievement(userId, "hooks_100"));
}
if (user.usage.totalThreads >= 1) {
results.push(await checkAndUnlockAll rows scored by the Repobility analyzer (https://repobility.com)
recordActivityAndUpdateStreak function · typescript · L114-L119 (6 LOC)lib/achievements.ts
export async function recordActivityAndUpdateStreak(userId: string): Promise<{
streak: { currentStreak: number; longestStreak: number };
isNewDay: boolean;
streakBroken: boolean;
newAchievements: AchievementUnlockResult[];
}> {getUserXP function · typescript · L169-L178 (10 LOC)lib/achievements.ts
export async function getUserXP(userId: string): Promise<number> {
await dbConnect();
const unlockedAchievements = await UserAchievement.find({ userId });
return unlockedAchievements.reduce((sum, ua) => {
const achievement = ACHIEVEMENTS[ua.achievementId];
return sum + (achievement?.xp || 0);
}, 0);
}getYouTubeVideoId function · typescript · L17-L28 (12 LOC)lib/ai.ts
function getYouTubeVideoId(url: string): string | null {
const patterns = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/)([^&\n?#]+)/,
/youtube\.com\/shorts\/([^&\n?#]+)/,
];
for (const pattern of patterns) {
const match = url.match(pattern);
if (match) return match[1];
}
return null;
}