Function bodies 106 total
AnalysisResult function · typescript · L585-L593 (9 LOC)apps/web/src/app/[lang]/analysis/result/page.tsx
export default function AnalysisResult() {
return (
<ErrorBoundary fallback={<CalculationErrorFallback />}>
<Suspense fallback={<div className="min-h-screen px-6 py-8 flex items-center justify-center"><div className="text-black dark:text-white">{/* Loading handled by AnalysisResultContent */}</div></div>}>
<AnalysisResultContent />
</Suspense>
</ErrorBoundary>
);
}useFetchPortfolioData function · typescript · L85-L136 (52 LOC)apps/web/src/app/[lang]/analysis/result/useFetchPortfolioData.ts
export function useFetchPortfolioData(portfolioId: string | null) {
const queryClient = useQueryClient();
const params = useSearchParams();
const portfolioQuery = useQuery<PortfolioData>({
queryKey: ['portfolio', portfolioId || params.get('tickers')],
queryFn: () => portfolioId ? fetchSavedPortfolio(portfolioId) : fetchFreshAnalysis(params),
retry: false,
staleTime: Infinity,
});
const reanalyzeMutation = useMutation({
mutationFn: (data: AnalyzePortfolioRequest) => analyzePortfolio(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['portfolio'] }),
});
const savePortfolioMutation = useMutation({
mutationFn: (data: CreatePortfolioRequest) => savePortfolio(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['portfolios'],
refetchType: 'none'
});
},
});
const updatePortfolioMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdatePortfolioRequest IntroductionLayout function · typescript · L13-L19 (7 LOC)apps/web/src/app/[lang]/introduction/layout.tsx
export default function IntroductionLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}IntroErrorFallback function · typescript · L10-L22 (13 LOC)apps/web/src/app/[lang]/introduction/page.tsx
function IntroErrorFallback() {
return (
<div className="min-h-screen flex items-center justify-center px-6">
<div className="glass-card p-8 max-w-md text-center space-y-4">
<h1 className="text-2xl font-bold text-black dark:text-white">Failed to Load Introduction</h1>
<p className="text-black/70 dark:text-white/70">Please try again or contact support.</p>
<LocalizedLink href="/" className="glass-button inline-block px-6 py-3">
Back to Home
</LocalizedLink>
</div>
</div>
);
}IntroductionPage function · typescript · L527-L533 (7 LOC)apps/web/src/app/[lang]/introduction/page.tsx
export default function IntroductionPage() {
return (
<ErrorBoundary fallback={<IntroErrorFallback />}>
<IntroductionContent />
</ErrorBoundary>
);
}generateMetadata function · typescript · L25-L76 (52 LOC)apps/web/src/app/[lang]/layout.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang } = await params;
const titles: Record<Language, string> = {
en: 'Glassbox - Portfolio Optimization Tool',
ko: 'Glassbox - 포트폴리오 최적화 도구',
};
const descriptions: Record<Language, string> = {
en: 'Transparent portfolio optimization and beta hedging with Glass UI design. Calculate efficient frontiers, optimize allocations, and hedge market risk.',
ko: '투명한 포트폴리오 최적화 및 베타 헤징 - Glass UI 디자인. 효율적 투자선을 계산하고, 자산 배분을 최적화하며, 시장 리스크를 헤징하세요.',
};
// Use absolute URLs for better SEO (requires NEXT_PUBLIC_SITE_URL env variable)
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://glassbox.space';
return {
title: titles[lang as Language] || titles.en,
description: descriptions[lang as Language] || descriptions.en,
icons: {
icon: '/favicon.svg',
},
alternates: {
languages: {
en: `${siteUrl}/en`,
ko: `${siteUrl}/ko`,
LangLayout function · typescript · L78-L112 (35 LOC)apps/web/src/app/[lang]/layout.tsx
export default async function LangLayout({ children, params }: Props) {
const { lang } = await params;
// Validate language - return 404 if not supported
if (!isSupportedLanguage(lang)) {
notFound();
}
// Use absolute URLs for hreflang tags (better SEO)
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://glassbox.space';
return (
<html lang={lang} suppressHydrationWarning>
<head>
{/* hreflang tags for SEO - must use absolute URLs */}
<link rel="alternate" hrefLang="en" href={`${siteUrl}/en`} />
<link rel="alternate" hrefLang="ko" href={`${siteUrl}/ko`} />
<link rel="alternate" hrefLang="x-default" href={`${siteUrl}/en`} />
{/* Theme initialization script */}
<script
dangerouslySetInnerHTML={{
__html: `(function(){try{const t=localStorage.getItem('theme')||'system';const d=t==='dark'||(t==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);if(d)document.documeSource: Repobility analyzer · https://repobility.com
LoginPage function · typescript · L229-L239 (11 LOC)apps/web/src/app/[lang]/login/page.tsx
export default function LoginPage({ params }: { params: Promise<{ lang: string }> }) {
return (
<Suspense fallback={
<div className="min-h-screen flex items-center justify-center">
<div className="w-8 h-8 border-4 border-cyan-500 border-t-transparent rounded-full animate-spin" />
</div>
}>
<LoginContent params={params} />
</Suspense>
);
}Home function · typescript · L434-L440 (7 LOC)apps/web/src/app/[lang]/page.tsx
export default function Home() {
return (
<ErrorBoundary fallback={<LandingErrorFallback />}>
<HomeContent />
</ErrorBoundary>
);
}PasswordResetPage function · typescript · L9-L108 (100 LOC)apps/web/src/app/[lang]/password-reset/page.tsx
export default function PasswordResetPage() {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [isSent, setIsSent] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
// Simulate password reset email sending
await new Promise(resolve => setTimeout(resolve, 2000));
setIsLoading(false);
setIsSent(true);
};
return (
<main className="min-h-screen flex items-center justify-center relative overflow-hidden py-20 px-4 bg-slate-50 dark:bg-slate-950">
{/* Background Elements */}
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-20 left-10 w-72 h-72 bg-purple-500/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-40 right-20 w-96 h-96 bg-cyan-500/10 rounded-full blur-3xl animate-pulse delay-1000"></div>
<div className="absolute top-1/2 left-1/2 -trAnalysisSettings function · typescript · L13-L64 (52 LOC)apps/web/src/app/[lang]/portfolio/new/components/AnalysisSettings.tsx
export function AnalysisSettings({ dateRange, setDateRange, variant = 'desktop' }: AnalysisSettingsProps) {
const { t } = useTranslation();
const isMobile = variant === 'mobile';
const content = (
<div className={isMobile ? 'space-y-4' : 'space-y-2'}>
<label className={`flex items-center gap-2 ${isMobile ? 'text-sm' : 'text-xs'} font-semibold text-black dark:text-white`}>
<Lightbulb className={`${isMobile ? 'w-4 h-4' : 'w-3 h-3'} text-cyan-500`} />
{t('portfolio.builder.analysis.settings.label')}
<Tooltip content={t('portfolio.builder.analysis.settings.tooltip')} width={250}>
<Info className={`${isMobile ? 'w-4 h-4' : 'w-3 h-3'} text-black/40 dark:text-white/40 cursor-help`} />
</Tooltip>
</label>
<div className={isMobile ? 'grid grid-cols-2 gap-3' : 'space-y-3'}>
<div>
<p className={`text-xs text-black/60 dark:text-white/60 ${isMobile ? 'mb-2' : 'mb-1'}`}>
{t('portfolio.builder.analysAssetList function · typescript · L14-L126 (113 LOC)apps/web/src/app/[lang]/portfolio/new/components/AssetList.tsx
export function AssetList({ items, colors, onRemove, onUpdateQuantity }: AssetListProps) {
const { t } = useTranslation();
if (items.length === 0) return null;
return (
<div className="space-y-3">
{items.map((item, index) => {
const color = colors[index % colors.length];
return (
<div
key={item.symbol}
className="group relative overflow-hidden rounded-2xl border border-white/40 dark:border-white/10 bg-white/40 dark:bg-black/20 backdrop-blur-xl transition-all duration-300 hover:scale-[1.01] hover:shadow-lg"
>
{/* Dynamic Gradient Background */}
<div
className="absolute inset-0 opacity-10 group-hover:opacity-20 transition-opacity duration-300"
style={{ background: `linear-gradient(135deg, ${color} 0%, transparent 100%)` }}
/>
<div className="relative p-4 flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-5">
AuthRequiredDialog function · typescript · L17-L62 (46 LOC)apps/web/src/app/[lang]/portfolio/new/components/AuthRequiredDialog.tsx
export function AuthRequiredDialog({ isOpen, onClose, pathname }: AuthRequiredDialogProps) {
const { t } = useTranslation();
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in">
<div className="bg-white dark:bg-slate-900/95 backdrop-blur-xl rounded-2xl shadow-2xl max-w-md w-full p-6 space-y-6 border border-slate-200 dark:border-cyan-500/20">
<div className="flex items-start gap-4">
<div className="p-3 rounded-full bg-cyan-100 dark:bg-cyan-900/30">
<Rocket className="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
</div>
<div className="flex-1">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2">
{t('auth.required.title')}
</h3>
<p className="text-sm text-slate-600 dark:text-slate-300 leading-relaxed">
{t('auth.required.message')}
BuilderErrorFallback function · typescript · L7-L43 (37 LOC)apps/web/src/app/[lang]/portfolio/new/components/BuilderErrorFallback.tsx
export function BuilderErrorFallback() {
const { t } = useTranslation();
return (
<main className="min-h-screen px-6 py-8 flex items-center justify-center">
<div className="glass-panel p-8 max-w-md w-full text-center space-y-6 border-orange-500/20 bg-orange-500/5">
<div className="mx-auto w-16 h-16 rounded-full bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center text-orange-500">
<AlertCircle className="w-8 h-8" />
</div>
<div>
<h2 className="text-xl font-bold text-black dark:text-white mb-2">
{t('portfolio.builder.error.title')}
</h2>
<p className="text-sm text-black/60 dark:text-white/60">
{t('portfolio.builder.error.description')}
</p>
</div>
<div className="flex gap-3 justify-center">
<Link
href="/"
className="px-4 py-2 rounded-lg bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 text-bMobileBottomBar function · typescript · L12-L43 (32 LOC)apps/web/src/app/[lang]/portfolio/new/components/MobileBottomBar.tsx
export function MobileBottomBar({ itemCount, validationError, onAnalyze }: MobileBottomBarProps) {
const { t } = useTranslation();
return (
<div className="fixed bottom-0 left-0 right-0 p-4 bg-white/80 dark:bg-black/80 backdrop-blur-xl border-t border-white/20 z-50 lg:hidden">
<div className="flex items-center gap-4 max-w-lg mx-auto">
<div className="flex-1">
<p className="text-xs text-black/50 dark:text-white/50 font-medium">
{t('portfolio.builder.summary.label')}
</p>
<div className="flex items-baseline gap-2">
<span className="text-lg font-bold text-black dark:text-white">
{itemCount} {t('portfolio.builder.summary.assets')}
</span>
<span className="text-sm text-black/60 dark:text-white/60">~ 00k</span>
</div>
</div>
<button
onClick={onAnalyze}
disabled={itemCount === 0 || !!validationError}
className="glass-buttGenerated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
PageHeader function · typescript · L9-L36 (28 LOC)apps/web/src/app/[lang]/portfolio/new/components/PageHeader.tsx
export function PageHeader() {
const { t } = useTranslation();
return (
<div className="space-y-6">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-black/10 dark:bg-white/10 border border-black/20 dark:border-white/20 backdrop-blur-sm">
<span className="w-2 h-2 rounded-full bg-purple-400 animate-pulse"></span>
<span className="text-sm font-medium text-black dark:text-white/80">
{t('portfolio.builder.step')}
</span>
</div>
<div className="space-y-4">
<h1 className="text-5xl sm:text-6xl font-bold text-black dark:text-white">
{t('portfolio.builder.title.part1')}
<br />
<span className="bg-gradient-to-r from-purple-300 to-cyan-300 bg-clip-text text-transparent">
{t('portfolio.builder.title.part2')}
</span>
</h1>
<p className="text-xl text-black dark:text-white/70 max-w-2xl">
{t('portfolio.builder.description')}
PortfolioDonutChart function · typescript · L17-L90 (74 LOC)apps/web/src/app/[lang]/portfolio/new/components/PortfolioDonutChart.tsx
export function PortfolioDonutChart({ items, colors }: PortfolioDonutChartProps) {
const { t } = useTranslation();
const data = useMemo(() => {
if (items.length === 0) return [{ name: t('portfolio.chart.empty'), value: 1 }];
return items.map((item) => ({
name: item.symbol,
value: item.quantity,
}));
}, [items, t]);
const isEmpty = items.length === 0;
return (
<div className="h-[280px] w-full relative group [&_*]:outline-none [&_*]:focus:outline-none">
{/* Background Glow Effect */}
<div className="absolute inset-0 bg-gradient-to-b from-cyan-400/5 to-purple-400/5 rounded-full blur-3xl scale-75 animate-pulse"></div>
<ResponsiveContainer width="100%" height="100%" debounce={50}>
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={80}
outerRadius={100}
paddingAngle={isEmpty ? 0 : 4}
dataKey="value"
stroke="none"
PortfolioSummaryPanel function · typescript · L34-L114 (81 LOC)apps/web/src/app/[lang]/portfolio/new/components/PortfolioSummaryPanel.tsx
export function PortfolioSummaryPanel({
items,
colors,
dateRange,
setDateRange,
validationError,
onAnalyze,
}: PortfolioSummaryPanelProps) {
const { t } = useTranslation();
return (
<div className="hidden lg:block lg:col-span-4 sticky top-24">
<div className="glass-panel p-6 space-y-6">
<h3 className="text-xl font-bold text-black dark:text-white flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-cyan-400" />
{t('portfolio.builder.preview.title')}
</h3>
{/* Donut Chart */}
<PortfolioDonutChart items={items} colors={colors} />
{/* Stats Summary */}
{items.length > 0 && (
<div className="grid grid-cols-2 gap-3 py-4 border-t border-b border-black/10 dark:border-white/10">
<div>
<p className="text-xs text-black/50 dark:text-white/50">
{t('portfolio.builder.preview.total')}
</p>
<p className="text-lg font-bQuickAddAssets function · typescript · L27-L92 (66 LOC)apps/web/src/app/[lang]/portfolio/new/components/QuickAddAssets.tsx
export function QuickAddAssets({ onAdd }: QuickAddAssetsProps) {
const { t } = useTranslation();
return (
<div className="space-y-5">
<div className="flex items-center gap-3 pb-2">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-cyan-500 to-purple-500 flex items-center justify-center">
<Sparkles className="w-4 h-4 text-white" />
</div>
<label className="text-sm font-bold text-black dark:text-white">
{t('portfolio.quick-add.title')}
</label>
</div>
<div className="flex-1 h-px bg-gradient-to-r from-cyan-500/30 to-transparent rounded-full"></div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
{ASSETS.map((asset) => {
const colorConfig = COLOR_MAP[asset.color] || COLOR_MAP.cyan;
const Icon = asset.icon;
return (
<button
key={assetStarterTemplates function · typescript · L75-L107 (33 LOC)apps/web/src/app/[lang]/portfolio/new/components/StarterTemplates.tsx
export function StarterTemplates({ onSelect }: StarterTemplatesProps) {
const { t } = useTranslation();
return (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{TEMPLATES.map((template) => {
const Icon = template.icon;
return (
<button
key={template.id}
onClick={() => onSelect(template.items)}
className={`glass-card-gradient ${template.color} text-left p-5 group hover:scale-[1.02] transition-all`}
>
<div className="flex items-start justify-between mb-3">
<div className="p-3 rounded-xl bg-white/20 dark:bg-black/20 backdrop-blur-md">
<Icon className="w-6 h-6 text-black dark:text-white" />
</div>
<span className="text-xs font-bold px-2 py-1 rounded-full bg-white/20 dark:bg-black/20 text-black dark:text-white">
{template.items.length} {t('portfolio.template.assets')}
</span>
</div>
useDialogs function · typescript · L7-L25 (19 LOC)apps/web/src/app/[lang]/portfolio/new/hooks/useDialogs.ts
export function useDialogs() {
const [showTodayWarning, setShowTodayWarning] = useState(false);
const [showAuthDialog, setShowAuthDialog] = useState(false);
const [showClearConfirm, setShowClearConfirm] = useState(false);
return {
// Today warning dialog
showTodayWarning,
setShowTodayWarning,
// Auth required dialog
showAuthDialog,
setShowAuthDialog,
// Clear confirmation dialog
showClearConfirm,
setShowClearConfirm,
};
}PortfolioBuilder function · typescript · L272-L278 (7 LOC)apps/web/src/app/[lang]/portfolio/new/page.tsx
export default function PortfolioBuilder() {
return (
<ErrorBoundary fallback={<BuilderErrorFallback />}>
<PortfolioBuilderContent />
</ErrorBoundary>
);
}usePortfolioBuilder function · typescript · L13-L94 (82 LOC)apps/web/src/app/[lang]/portfolio/new/usePortfolioBuilder.ts
export function usePortfolioBuilder() {
// Portfolio state
const [items, setItems] = useState<PortfolioItem[]>([]);
const [dateRange, setDateRange] = useState({ startDate: '', endDate: '' });
// Search state
const [searchInput, setSearchInput] = useState('');
const debouncedSearchInput = useDebounce(searchInput, 300);
const [showDropdown, setShowDropdown] = useState(false);
// Storage hook
const { restoreDraft, saveDraft, hasRestoredDraft, setHasRestoredDraft } = usePortfolioStorage();
// Restore portfolio from sessionStorage on mount
// Guard against double-restore (React Strict Mode, hot reload)
useEffect(() => {
if (hasRestoredDraft) return; // Already restored, skip
const draft = restoreDraft();
if (draft) {
setItems(draft.items);
setDateRange(draft.dateRange);
setHasRestoredDraft(true);
}
}, [restoreDraft, hasRestoredDraft, setHasRestoredDraft]);
// Query for searching tickers
const searchQuery = useQuery({
qRepobility · code-quality intelligence platform · https://repobility.com
PortfolioCard function · typescript · L18-L138 (121 LOC)apps/web/src/app/[lang]/portfolios/components/PortfolioCard.tsx
export function PortfolioCard({ portfolio, onDelete, isDeleting, colors }: PortfolioCardProps) {
const { t } = useTranslation();
const params = useParams();
const locale = (params?.lang as DateLocale) || 'en';
const stats = portfolio.analysisSnapshot?.maxSharpe?.stats;
// Calculate composition based on quantities (simple visual approximation)
const totalQuantity = portfolio.quantities.reduce((a, b) => a + b, 0);
const composition = portfolio.tickers.map((ticker, index) => ({
ticker,
percentage: (portfolio.quantities[index] / totalQuantity) * 100,
color: colors[index % colors.length]
}));
return (
<div className={`glass-panel p-5 group hover:border-cyan-500/30 transition-all flex flex-col h-full ${isDeleting ? 'opacity-50 pointer-events-none' : ''}`}>
{/* Header */}
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-lg font-bold text-black dark:text-white group-hover:text-cyan-600 dark:groupLibraryErrorFallback function · typescript · L24-L60 (37 LOC)apps/web/src/app/[lang]/portfolios/page.tsx
function LibraryErrorFallback() {
const { t } = useTranslation();
return (
<main className="min-h-screen px-6 py-8 flex items-center justify-center">
<div className="glass-panel p-8 max-w-md w-full text-center space-y-6 border-orange-500/20 bg-orange-500/5">
<div className="mx-auto w-16 h-16 rounded-full bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center text-orange-500">
<AlertCircle className="w-8 h-8" />
</div>
<div>
<h2 className="text-xl font-bold text-black dark:text-white mb-2">
{t('portfolio.library.error.title')}
</h2>
<p className="text-sm text-black/60 dark:text-white/60">
{t('portfolio.library.error.description')}
</p>
</div>
<div className="flex gap-3 justify-center">
<Link
href="/"
className="px-4 py-2 rounded-lg bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 text-black daPortfolioLibraryContent function · typescript · L62-L261 (200 LOC)apps/web/src/app/[lang]/portfolios/page.tsx
function PortfolioLibraryContent() {
const { t } = useTranslation();
const { data: session, status } = useSession();
const queryClient = useQueryClient();
const [searchQuery, setSearchQuery] = useState('');
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [portfolioToDelete, setPortfolioToDelete] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const isAuthenticated = status === 'authenticated';
const isAuthLoading = status === 'loading';
// Fetch portfolios with useQuery - only runs when authenticated
const { data: portfolios = [], isLoading } = useQuery<Portfolio[]>({
queryKey: ['portfolios'],
queryFn: getAllPortfolios,
enabled: isAuthenticated, // Only fetch when authenticated
});
// Delete mutation with optimistic updates
const deleteMutation = useMutation({
mutationFn: deletePortfolio,
onMutate: async (portfolioId) => {
// Cancel outgoing refetches
PortfolioLibrary function · typescript · L263-L269 (7 LOC)apps/web/src/app/[lang]/portfolios/page.tsx
export default function PortfolioLibrary() {
return (
<ErrorBoundary fallback={<LibraryErrorFallback />}>
<PortfolioLibraryContent />
</ErrorBoundary>
);
}SignupPage function · typescript · L286-L296 (11 LOC)apps/web/src/app/[lang]/signup/page.tsx
export default function SignupPage() {
return (
<Suspense fallback={
<div className="min-h-screen flex items-center justify-center">
<div className="w-8 h-8 border-4 border-cyan-500 border-t-transparent rounded-full animate-spin" />
</div>
}>
<SignupContent />
</Suspense>
);
}ThemeProvider function · typescript · L18-L56 (39 LOC)apps/web/src/app/providers.tsx
export function ThemeProvider({ children, lang = 'en' }: { children: ReactNode; lang?: string }) {
const { theme, resolvedTheme, setTheme } = useThemeState();
// Create i18n instance once with the server-provided language
const [i18n] = useState(() => createI18nInstance(lang));
// Ensure QueryClient is created once per component lifecycle
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes (garbage collection)
retry: 1, // Retry failed requests once
refetchOnWindowFocus: false, // Don't refetch when window regains focus
},
mutations: {
retry: 1, // Retry failed mutations once
},
},
}));
const value = useMemo(() => ({
theme,
resolvedTheme,
setTheme
}), [theme, resolvedTheme, setTheme]);
return (
<SessionProvider>
<I18nextProvider i18n={i18n}>
<QueryClientProviuseTheme function · typescript · L58-L64 (7 LOC)apps/web/src/app/providers.tsx
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}robots function · typescript · L3-L14 (12 LOC)apps/web/src/app/robots.ts
export default function robots(): MetadataRoute.Robots {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://glassbox.space';
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/api/', '/profile/'],
},
sitemap: `${siteUrl}/sitemap.xml`,
};
}Repobility · MCP-ready · https://repobility.com
sitemap function · typescript · L4-L34 (31 LOC)apps/web/src/app/sitemap.ts
export default function sitemap(): MetadataRoute.Sitemap {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://glassbox.space';
const lastModified = new Date();
// Base routes to be localized
const routes = ['', '/portfolio/new', '/portfolios'];
const sitemapEntries: MetadataRoute.Sitemap = [];
// Add localized versions for each route
SUPPORTED_LANGUAGES.forEach((lang) => {
routes.forEach((route) => {
sitemapEntries.push({
url: `${siteUrl}/${lang}${route}`,
lastModified,
changeFrequency: route === '' ? 'weekly' : 'monthly',
priority: route === '' ? 1.0 : 0.8,
});
});
});
// Add root redirect path
sitemapEntries.push({
url: siteUrl,
lastModified,
changeFrequency: 'monthly',
priority: 0.5,
});
return sitemapEntries;
}BackButton function · typescript · L12-L23 (12 LOC)apps/web/src/components/back-button.tsx
export function BackButton({ href, label }: BackButtonProps) {
const { t } = useTranslation();
return (
<LocalizedLink
href={href}
className="text-sm font-semibold text-black/60 dark:text-white/60 hover:text-black dark:hover:text-white transition-colors duration-200 flex items-center gap-2 group"
>
<ArrowLeft className="w-4 h-4 transition-transform group-hover:-translate-x-0.5" />
<span className="hidden sm:inline">{label ?? t('common.button.back')}</span>
</LocalizedLink>
);
}ErrorBoundary.render method · typescript · L35-L84 (50 LOC)apps/web/src/components/error-boundary.tsx
public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-[400px] w-full flex items-center justify-center p-6">
<div className="glass-panel p-8 max-w-md w-full text-center space-y-6 border-red-500/20 bg-red-500/5">
<div className="mx-auto w-16 h-16 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center text-red-500 mb-4">
<AlertTriangle className="w-8 h-8" />
</div>
<div>
<h2 className="text-xl font-bold text-black dark:text-white mb-2">
Something went wrong
</h2>
<p className="text-sm text-black/60 dark:text-white/60 mb-4">
We encountered an unexpected error while rendering this component.
</p>
{this.state.error && (
<div className="bg-black/5 dark:bg-white/5 p-3ExportDropdown function · typescript · L18-L102 (85 LOC)apps/web/src/components/export-dropdown.tsx
export function ExportDropdown({
portfolioName,
items,
analysis,
align = 'right',
className = '',
showLabel = true
}: ExportDropdownProps) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
if (!analysis) return null;
const handleExport = (type: 'csv' | 'pdf') => {
try {
const exportData = {
portfolioName,
items,
analysis,
timestamp: new Date(),
};
if (type === 'csv') exportAsCSV(exportData);
else exportAsPDF(exportData);
} catch (error) {
// Failed silently as per console cleanup request
} finally {
setIsOpen(false);
}
};
return (
<div className={`relative ${className}`}>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsOpen(!isOpen);
}}
className="h-9 w-9 lg:w-auto lg:px-3 flex-shrink-0 flex items-center justify-center lg:justify-start gap-2 rounded-lg text-xs GlassboxIcon function · typescript · L1-L37 (37 LOC)apps/web/src/components/glassbox-icon.tsx
export function GlassboxIcon() {
return (
<svg
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-full"
fill="none"
>
<defs>
{/* Vibrant Gradient: Cyan -> Purple -> Pink */}
<linearGradient id="mainGrad" x1="64" y1="64" x2="448" y2="448" gradientUnits="userSpaceOnUse">
<stop offset="0" stopColor="#22d3ee"/> {/* Cyan 400 */}
<stop offset="0.5" stopColor="#c084fc"/> {/* Purple 400 */}
<stop offset="1" stopColor="#f472b6"/> {/* Pink 400 */}
</linearGradient>
{/* Glossy Reflection */}
<linearGradient id="glassShine" x1="256" y1="64" x2="256" y2="448" gradientUnits="userSpaceOnUse">
<stop offset="0" stopColor="white" stopOpacity="0.5"/>
<stop offset="1" stopColor="white" stopOpacity="0"/>
</linearGradient>
</defs>
{/* Isometric Cube Silhouette */}
<path d="M256 64L448 176V336L256 448L64 336V176L2HeroVisual function · typescript · L5-L93 (89 LOC)apps/web/src/components/landing/hero-visual.tsx
export function HeroVisual() {
return (
<div className="relative w-full max-w-[600px] mx-auto perspective-1000 group">
{/* Floating Elements Background */}
<div className="absolute -top-20 -right-20 w-64 h-64 bg-cyan-400/20 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute -bottom-20 -left-20 w-64 h-64 bg-purple-400/20 rounded-full blur-3xl animate-pulse delay-700"></div>
{/* Main Glass Dashboard Card - Tilted */}
<div
className="relative glass-panel p-6 transform rotate-y-12 rotate-x-6 transition-transform duration-700 group-hover:rotate-y-6 group-hover:rotate-x-3 shadow-2xl border-white/40 dark:border-white/10 bg-white/40 dark:bg-black/40 backdrop-blur-xl"
style={{ transformStyle: 'preserve-3d' }}
>
{/* Header Mockup */}
<div className="flex items-center justify-between mb-8 border-b border-white/10 pb-4">
<div className="flex items-center gap-3">
<div className="w-3 hLandingErrorFallback function · typescript · L6-L36 (31 LOC)apps/web/src/components/landing/LandingErrorFallback.tsx
export function LandingErrorFallback() {
const { t } = useTranslation();
return (
<main className="min-h-screen px-6 py-8 flex items-center justify-center">
<div className="glass-panel p-8 max-w-md w-full text-center space-y-6 border-orange-500/20 bg-orange-500/5">
<div className="mx-auto w-16 h-16 rounded-full bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center text-orange-500">
<AlertCircle className="w-8 h-8" />
</div>
<div>
<h2 className="text-xl font-bold text-black dark:text-white mb-2">
{t('landing.error.title')}
</h2>
<p className="text-sm text-black/60 dark:text-white/60">
{t('landing.error.message')}
</p>
</div>
<div className="flex gap-3 justify-center">
<button
onClick={() => window.location.reload()}
className="px-4 py-2 rounded-lg bg-orange-500 text-white hover:bg-orange-600 transition-colors fLocalizedLink function · typescript · L27-L35 (9 LOC)apps/web/src/components/LocalizedLink.tsx
function LocalizedLink({ href, ...props }, ref) {
const params = useParams();
const currentLang = (params?.lang as Language) || DEFAULT_LANGUAGE;
// Auto-prefix href with current language
const localizedHref = getLocalizedPath(href, currentLang);
return <Link ref={ref} href={localizedHref} {...props} />;
}Source: Repobility analyzer · https://repobility.com
SessionProvider function · typescript · L14-L22 (9 LOC)apps/web/src/components/SessionProvider.tsx
export function SessionProvider({ children, session }: { children: ReactNode; session?: Session | null }) {
return (
<NextAuthSessionProvider basePath="/auth" session={session}>
<OAuthSyncWrapper>
{children}
</OAuthSyncWrapper>
</NextAuthSessionProvider>
);
}Tooltip function · typescript · L12-L88 (77 LOC)apps/web/src/components/Tooltip.tsx
export function Tooltip({ children, content, width = 250 }: TooltipProps) {
const [isVisible, setIsVisible] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const triggerRef = useRef<HTMLDivElement>(null);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const updatePosition = useCallback(() => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
setPosition({
top: rect.top + window.scrollY - 8, // 8px gap above trigger
left: rect.left + window.scrollX + rect.width / 2, // Center horizontally
});
}
}, []);
const handleMouseEnter = () => {
updatePosition();
setIsVisible(true);
};
const handleMouseLeave = () => {
setIsVisible(false);
};
useEffect(() => {
if (!isVisible) {
return;
}
window.addEventListener('scroll', updatePosition);
window.addEventListener('resize', updatePouseDebounce function · typescript · L9-L23 (15 LOC)apps/web/src/hooks/useDebounce.ts
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}useLocalizedRouter function · typescript · L20-L37 (18 LOC)apps/web/src/hooks/useLocalizedRouter.ts
export function useLocalizedRouter() {
const router = useNextRouter();
const params = useParams();
const currentLang = (params?.lang as Language) || DEFAULT_LANGUAGE;
return {
...router,
currentLang,
push: (href: string, options?: any) => {
const localizedHref = getLocalizedPath(href, currentLang);
return router.push(localizedHref, options);
},
replace: (href: string, options?: any) => {
const localizedHref = getLocalizedPath(href, currentLang);
return router.replace(localizedHref, options);
},
};
}useOAuthSync function · typescript · L11-L64 (54 LOC)apps/web/src/hooks/useOAuthSync.ts
export function useOAuthSync() {
const { data: session, status } = useSession();
const syncedRef = useRef(false);
useEffect(() => {
// Only run once per component lifecycle
if (syncedRef.current) return;
// Wait for session to load
if (status !== 'authenticated' || !session?.user?.email) return;
// Make client-side call to get backend cookie
// We ALWAYS call this because we can't check if httpOnly cookie exists
const syncWithBackend = async () => {
try {
const backendUrl = process.env.NEXT_PUBLIC_API_URL;
if (!backendUrl) {
console.error('[OAuth Sync] NEXT_PUBLIC_API_URL not set');
return;
}
console.log('[OAuth Sync] Syncing backend cookie for:', session.user.email);
console.log('[OAuth Sync] Backend URL:', backendUrl);
const response = await fetch(`${backendUrl}/users/sync`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
crusePortfolioStorage function · typescript · L18-L76 (59 LOC)apps/web/src/hooks/usePortfolioStorage.ts
export function usePortfolioStorage() {
const [hasRestoredDraft, setHasRestoredDraft] = useState(false);
const saveDraft = useCallback((items: PortfolioItem[], dateRange: { startDate: string; endDate: string }) => {
try {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ items, dateRange }));
} catch (error) {
console.error('Failed to save portfolio draft:', error);
}
}, []);
const restoreDraft = useCallback((): PortfolioDraft | null => {
try {
const draft = sessionStorage.getItem(STORAGE_KEY);
if (!draft) return null;
const data = JSON.parse(draft) as PortfolioDraft;
// Comprehensive validation
const isValid =
data &&
typeof data === 'object' &&
Array.isArray(data.items) &&
data.dateRange &&
typeof data.dateRange === 'object' &&
typeof data.dateRange.startDate === 'string' &&
typeof data.dateRange.endDate === 'string';
if (isValid) {
sessionStuseTheme function · typescript · L16-L65 (50 LOC)apps/web/src/hooks/useTheme.ts
export function useTheme() {
const [theme, setThemeState] = useState<ThemePreference>('system');
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('light');
const [mounted, setMounted] = useState(false);
useEffect(() => {
const initialTheme = (localStorage.getItem('theme') as ThemePreference | null) || 'system';
const resolved = getResolvedTheme(initialTheme);
setThemeState(initialTheme);
setResolvedTheme(resolved);
applyResolvedTheme(resolved);
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
if (initialTheme === 'system') {
const nextTheme = e.matches ? 'dark' : 'light';
setResolvedTheme(nextTheme);
applyResolvedTheme(nextTheme);
}
};
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleChange);
} else if (mediaQuery.addListener) {
mediaQuery.addListener(handleChange);
}detectLanguage function · typescript · L14-L55 (42 LOC)apps/web/src/lib/detect-language.ts
export function detectLanguage(
acceptLanguageHeader: string,
supportedLanguages: string[] = ['en', 'ko'],
defaultLanguage: string = 'en'
): string {
if (!acceptLanguageHeader) {
return defaultLanguage;
}
// Parse Accept-Language header into array of { lang, quality }
const languages = acceptLanguageHeader
.split(',')
.map((lang) => {
const parts = lang.trim().split(';');
const code = parts[0].toLowerCase();
const qMatch = parts[1]?.match(/q=([0-9.]+)/);
const quality = qMatch ? parseFloat(qMatch[1]) : 1.0;
return { code, quality };
})
// Sort by quality (highest first)
.sort((a, b) => b.quality - a.quality);
// Find first language that matches our supported languages
for (const { code } of languages) {
// Extract base language code (e.g., 'ko' from 'ko-KR')
const baseCode = code.split('-')[0];
// Check if base code matches any supported language
if (supportedLanguages.includes(baseCode)) {
Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
getLocalizedPath function · typescript · L16-L22 (7 LOC)apps/web/src/lib/i18n/paths.ts
export function getLocalizedPath(path: string, lang: Language): string {
// Remove leading slash for consistency
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
// Build localized path
return `/${lang}${cleanPath ? `/${cleanPath}` : ''}`;
}stripLangFromPath function · typescript · L31-L40 (10 LOC)apps/web/src/lib/i18n/paths.ts
export function stripLangFromPath(path: string): string {
const segments = path.split('/').filter(Boolean);
// If first segment is a language, remove it
if (segments.length > 0 && SUPPORTED_LANGUAGES.includes(segments[0] as Language)) {
segments.shift();
}
return `/${segments.join('/')}`;
}extractLangFromPath function · typescript · L50-L58 (9 LOC)apps/web/src/lib/i18n/paths.ts
export function extractLangFromPath(path: string): Language | null {
const segments = path.split('/').filter(Boolean);
if (segments.length > 0 && SUPPORTED_LANGUAGES.includes(segments[0] as Language)) {
return segments[0] as Language;
}
return null;
}