Skip to content

Commit 610d3de

Browse files
author
Leck88
committed
fix: graceful fallback when MongoDB unavailable at build time
1 parent e068027 commit 610d3de

14 files changed

Lines changed: 481 additions & 60 deletions

File tree

app/layout.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { Geist, Geist_Mono } from "next/font/google";
33
import { Analytics } from "@vercel/analytics/next";
44
import {Toaster} from "@/components/ui/sonner";
55
import "./globals.css";
6+
import { cookies } from "next/headers";
7+
import { LocaleProvider } from "@/components/LocaleProvider";
8+
import { getDictionary, type Locale } from "@/i18n";
69

710
const geistSans = Geist({
811
variable: "--font-geist-sans",
@@ -19,17 +22,27 @@ export const metadata: Metadata = {
1922
description: "OpenStock is an open-source alternative to expensive market platforms. Track real-time prices, set personalized alerts, and explore detailed company insights — built openly, for everyone, forever free.",
2023
};
2124

22-
export default function RootLayout({
25+
const locales: Locale[] = ['en', 'zh-CN'];
26+
const defaultLocale: Locale = 'en';
27+
28+
export default async function RootLayout({
2329
children,
2430
}: Readonly<{
2531
children: React.ReactNode;
2632
}>) {
33+
const cookieStore = await cookies();
34+
const localeCookie = cookieStore.get('NEXT_LOCALE')?.value as Locale;
35+
const locale: Locale = localeCookie && locales.includes(localeCookie) ? localeCookie : defaultLocale;
36+
const dictionary = getDictionary(locale);
37+
2738
return (
28-
<html lang="en" className="dark">
39+
<html lang={locale} className="dark">
2940
<body
3041
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
3142
>
32-
{children}
43+
<LocaleProvider locale={locale} dictionary={dictionary}>
44+
{children}
45+
</LocaleProvider>
3346
<Toaster/>
3447
<Analytics />
3548
</body>

components/Footer.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
'use client';
2+
13
import Link from "next/link";
24
import Image from "next/image";
35
import OpenDevSocietyBranding from "./OpenDevSocietyBranding";
6+
import { useDictionary } from "@/hooks/useDictionary";
47

58
const Footer = () => {
9+
const dict = useDictionary();
10+
611
return (
712
<footer className="bg-gray-900 text-white border-t border-gray-800">
813
<div className="container mx-auto px-4 py-12">
@@ -19,11 +24,11 @@ const Footer = () => {
1924
/>
2025
</Link>
2126
<p className="text-gray-400 mb-6 max-w-md">
22-
OpenStock is an open-source alternative to expensive market platforms. Track real-time prices, set personalized alerts, and explore detailed company insights — built openly, for everyone, forever free.
27+
{dict.footer.description}
2328
</p>
2429
<div className="mb-8">
2530
<Link href="/about" className="text-teal-400 hover:text-teal-300 font-medium inline-flex items-center gap-1 group">
26-
Learn about our mission
31+
{dict.footer.learnMore}
2732
<span className="group-hover:translate-x-1 transition-transform"></span>
2833
</Link>
2934
</div>
@@ -35,7 +40,7 @@ const Footer = () => {
3540
className="text-gray-400 hover:text-white transition-colors duration-200 relative group"
3641
>
3742
<span className="relative">
38-
GitHub
43+
{dict.footer.github}
3944
<span className="absolute left-0 bottom-0 w-0 h-0.5 bg-white transition-all duration-300 group-hover:w-full"></span>
4045
</span>
4146
</Link>
@@ -46,7 +51,7 @@ const Footer = () => {
4651
className="text-gray-400 hover:text-blue-400 transition-colors duration-200 relative group"
4752
>
4853
<span className="relative">
49-
LinkedIn
54+
{dict.footer.linkedin}
5055
<span className="absolute left-0 bottom-0 w-0 h-0.5 bg-blue-400 transition-all duration-300 group-hover:w-full"></span>
5156
</span>
5257
</Link>
@@ -57,7 +62,7 @@ const Footer = () => {
5762
className="text-gray-400 hover:text-blue-600 transition-colors duration-200 relative group"
5863
>
5964
<span className="relative">
60-
Discord
65+
{dict.footer.discord}
6166
<span className="absolute left-0 bottom-0 w-0 h-0.5 bg-blue-600 transition-all duration-300 group-hover:w-full"></span>
6267
</span>
6368
</Link>
@@ -66,28 +71,28 @@ const Footer = () => {
6671

6772
{/* Resources */}
6873
<div>
69-
<h3 className="text-lg font-semibold mb-4">Resources</h3>
74+
<h3 className="text-lg font-semibold mb-4">{dict.footer.resources}</h3>
7075
<ul className="space-y-2">
7176
<li>
7277
<Link href="/api-docs" className="text-gray-400 hover:text-white transition-colors duration-200 relative group">
7378
<span className="relative">
74-
API Documentation
79+
{dict.footer.apiDocumentation}
7580
<span className="absolute left-0 bottom-0 w-0 h-0.5 bg-white transition-all duration-300 group-hover:w-full"></span>
7681
</span>
7782
</Link>
7883
</li>
7984
<li>
8085
<Link href="/help" className="text-gray-400 hover:text-white transition-colors duration-200 relative group">
8186
<span className="relative">
82-
Help Center
87+
{dict.footer.helpCenter}
8388
<span className="absolute left-0 bottom-0 w-0 h-0.5 bg-white transition-all duration-300 group-hover:w-full"></span>
8489
</span>
8590
</Link>
8691
</li>
8792
<li>
8893
<Link href="/terms" className="text-gray-400 hover:text-white transition-colors duration-200 relative group">
8994
<span className="relative">
90-
Terms of Service
95+
{dict.footer.termsOfService}
9196
<span className="absolute left-0 bottom-0 w-0 h-0.5 bg-white transition-all duration-300 group-hover:w-full"></span>
9297
</span>
9398
</Link>
@@ -101,7 +106,7 @@ const Footer = () => {
101106
<div className="flex flex-col md:flex-row justify-between items-center">
102107
{/* Copyright */}
103108
<div className="text-gray-400 text-sm mb-4 md:mb-0">
104-
© {new Date().getFullYear()} Open Dev Society. All rights reserved.
109+
© {new Date().getFullYear()} {dict.footer.copyright}
105110
</div>
106111

107112
{/* Open Dev Society Branding */}

components/LanguageSwitcher.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use client';
2+
3+
import { useTransition } from 'react';
4+
import { useRouter, usePathname } from 'next/navigation';
5+
import { Globe } from 'lucide-react';
6+
import {
7+
Select,
8+
SelectContent,
9+
SelectItem,
10+
SelectTrigger,
11+
SelectValue,
12+
} from '@/components/ui/select';
13+
import { useLocale } from '@/hooks/useLocale';
14+
15+
export default function LanguageSwitcher() {
16+
const router = useRouter();
17+
const pathname = usePathname();
18+
const locale = useLocale();
19+
const [isPending, startTransition] = useTransition();
20+
21+
const handleLocaleChange = (newLocale: string) => {
22+
startTransition(() => {
23+
const segments = pathname.split('/');
24+
// Remove existing locale segment if present
25+
if (segments[1] === 'en' || segments[1] === 'zh-CN') {
26+
segments[1] = newLocale;
27+
} else {
28+
segments.splice(1, 0, newLocale);
29+
}
30+
router.push(segments.join('/'));
31+
document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`;
32+
});
33+
};
34+
35+
return (
36+
<Select value={locale} onValueChange={handleLocaleChange} disabled={isPending}>
37+
<SelectTrigger className="w-[130px] h-9 bg-gray-700 border-gray-600 text-gray-300 hover:bg-gray-600 hover:text-white">
38+
<Globe className="h-4 w-4 mr-2" />
39+
<SelectValue placeholder="Language" />
40+
</SelectTrigger>
41+
<SelectContent className="bg-gray-800 border-gray-700 text-gray-200">
42+
<SelectItem value="en" className="hover:bg-gray-700 focus:bg-gray-700">
43+
English
44+
</SelectItem>
45+
<SelectItem value="zh-CN" className="hover:bg-gray-700 focus:bg-gray-700">
46+
简体中文
47+
</SelectItem>
48+
</SelectContent>
49+
</Select>
50+
);
51+
}

components/LocaleProvider.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import React, { createContext, useEffect, useState } from 'react';
4+
import { type Locale, getDictionary } from '@/i18n';
5+
import type { Dictionary } from '@/i18n/en';
6+
7+
interface LocaleContextType {
8+
locale: Locale;
9+
dictionary: Dictionary;
10+
setLocale: (locale: Locale) => void;
11+
}
12+
13+
export const LocaleContext = createContext<LocaleContextType>({
14+
locale: 'en',
15+
dictionary: getDictionary('en'),
16+
setLocale: () => {},
17+
});
18+
19+
interface LocaleProviderProps {
20+
children: React.ReactNode;
21+
locale: Locale;
22+
dictionary: Dictionary;
23+
}
24+
25+
export function LocaleProvider({ children, locale: initialLocale, dictionary }: LocaleProviderProps) {
26+
const [locale, setLocale] = useState<Locale>(initialLocale);
27+
const [currentDictionary, setCurrentDictionary] = useState<Dictionary>(dictionary);
28+
29+
useEffect(() => {
30+
setCurrentDictionary(getDictionary(locale));
31+
}, [locale]);
32+
33+
const changeLocale = (newLocale: Locale) => {
34+
setLocale(newLocale);
35+
document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`;
36+
};
37+
38+
return (
39+
<LocaleContext.Provider value={{ locale, dictionary: currentDictionary, setLocale: changeLocale }}>
40+
{children}
41+
</LocaleContext.Provider>
42+
);
43+
}

components/NavItems.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33

44
import React, { createContext, useContext } from 'react'
5-
import {NAV_ITEMS} from "@/lib/constants";
65
import Link from "next/link";
76
import {usePathname} from "next/navigation";
87
import SearchCommand from "@/components/SearchCommand";
98
import { Heart } from 'lucide-react';
109
import { Button } from '@/components/ui/button';
10+
import { useDictionary } from '@/hooks/useDictionary';
1111

1212
// Create context for popup state
1313
const DonatePopupContext = createContext<{
@@ -20,6 +20,7 @@ export const useDonatePopup = () => useContext(DonatePopupContext);
2020

2121
const NavItems = ({initialStocks}: { initialStocks: StockWithWatchlistStatus[]}) => {
2222
const pathname = usePathname()
23+
const dict = useDictionary();
2324

2425
const isActive = (path: string) => {
2526
if (path ==='/') return pathname === '/'
@@ -35,12 +36,17 @@ const NavItems = ({initialStocks}: { initialStocks: StockWithWatchlistStatus[]})
3536
return (
3637
<DonatePopupContext.Provider value={{ openDonatePopup }}>
3738
<ul className="flex flex-col sm:flex-row p-2 gap-3 sm:gap-10 font-medium">
38-
{NAV_ITEMS.map(({href, label}) => {
39+
{dict.nav && [
40+
{ href: '/', label: dict.nav.dashboard },
41+
{ href: '/search', label: dict.nav.search },
42+
{ href: '/watchlist', label: dict.nav.watchlist },
43+
{ href: '/api-docs', label: dict.nav.apiDocs },
44+
].map(({href, label}) => {
3945
if (href === '/search') return (
4046
<li key="search-trigger">
4147
<SearchCommand
4248
renderAs="text"
43-
label="Search"
49+
label={dict.common.search}
4450
initialStocks={initialStocks}
4551
/>
4652
</li>
@@ -58,7 +64,7 @@ const NavItems = ({initialStocks}: { initialStocks: StockWithWatchlistStatus[]})
5864
size="sm"
5965
>
6066
<Heart className="h-4 w-4 fill-current" />
61-
Donate
67+
{dict.nav.donate}
6268
</Button>
6369
</li>
6470
</ul>

database/mongoose.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import mongoose from "mongoose";
33
const MONGODB_URI = process.env.MONGODB_URI;
44

55
// FIX: Set Google DNS and force IPv4 to avoid querySrv ECONNREFUSED
6-
import dns from 'dns';
6+
import * as dns from 'dns';
77
try {
88
// This is often more effective than setServers for Node 17+
99
if (dns.setDefaultResultOrder) {
@@ -30,7 +30,8 @@ if (!cached) {
3030

3131
export const connectToDatabase = async () => {
3232
if (!MONGODB_URI) {
33-
throw new Error("MongoDB URI is missing");
33+
console.warn("MongoDB URI is missing — skipping database connection");
34+
return null;
3435
}
3536

3637
if (cached.conn) return cached.conn;

hooks/useDictionary.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use client';
2+
3+
import { useContext } from 'react';
4+
import { LocaleContext } from '@/components/LocaleProvider';
5+
import type { Dictionary } from '@/i18n/en';
6+
7+
export function useDictionary(): Dictionary {
8+
const { dictionary } = useContext(LocaleContext);
9+
return dictionary;
10+
}

hooks/useLocale.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use client';
2+
3+
import { useContext } from 'react';
4+
import { LocaleContext } from '@/components/LocaleProvider';
5+
import type { Locale } from '@/i18n';
6+
7+
export function useLocale(): Locale {
8+
const { locale } = useContext(LocaleContext);
9+
return locale;
10+
}

0 commit comments

Comments
 (0)