268 lines
9.4 KiB
TypeScript
268 lines
9.4 KiB
TypeScript
import React, { useContext, useState } from "react";
|
|
import { HashRouter, Routes, Route, Link, useLocation } from "react-router-dom";
|
|
import { Home } from "./pages/Home";
|
|
import { BlogPost } from "./pages/BlogPost";
|
|
import { Research } from "./pages/Research";
|
|
import { Services } from "./pages/Services";
|
|
import { HireUs } from "./pages/HireUs";
|
|
import { Playground } from "./pages/Playground";
|
|
import { ChatWidget } from "./components/ChatWidget";
|
|
import { Cpu, Menu, X, Globe, Terminal } from "lucide-react";
|
|
|
|
// --- Language Context ---
|
|
type Language = "en" | "th";
|
|
|
|
interface LanguageContextType {
|
|
language: Language;
|
|
toggleLanguage: () => void;
|
|
}
|
|
|
|
export const LanguageContext = React.createContext<LanguageContextType>({
|
|
language: "en",
|
|
toggleLanguage: () => {},
|
|
});
|
|
|
|
export const useLanguage = () => useContext(LanguageContext);
|
|
|
|
// --- Navigation ---
|
|
const Navigation: React.FC = () => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const location = useLocation();
|
|
const { language, toggleLanguage } = useLanguage();
|
|
|
|
const isActive = (path: string) => {
|
|
if (path === "/research" && location.pathname.startsWith("/research")) {
|
|
return "text-ink font-semibold";
|
|
}
|
|
return location.pathname === path
|
|
? "text-ink font-semibold"
|
|
: "text-subtle hover:text-ink transition-colors";
|
|
};
|
|
|
|
return (
|
|
<nav className="sticky top-0 z-50 bg-paper/95 backdrop-blur-sm border-b border-gray-100">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between h-20">
|
|
<div className="flex items-center">
|
|
<Link to="/" className="flex items-center space-x-2">
|
|
<Cpu className="h-6 w-6 text-ink" />
|
|
<span className="font-sans tracking-widest text-sm font-bold uppercase text-ink">
|
|
Pradit
|
|
</span>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Desktop Nav */}
|
|
<div className="hidden md:flex items-center space-x-8 font-sans text-sm">
|
|
<Link to="/" className={isActive("/")}>
|
|
{language === "en" ? "Work" : "ผลงาน"}
|
|
</Link>
|
|
<Link to="/services" className={isActive("/services")}>
|
|
{language === "en" ? "Services" : "บริการ"}
|
|
</Link>
|
|
<Link to="/research" className={isActive("/research")}>
|
|
{language === "en" ? "Research" : "วิจัย"}
|
|
</Link>
|
|
<Link
|
|
to="/playground"
|
|
className={`${isActive("/playground")} flex items-center gap-1`}
|
|
>
|
|
<Terminal size={14} />
|
|
{language === "en" ? "Playground" : "ลองเล่น"}
|
|
</Link>
|
|
<Link
|
|
to="/hire-us"
|
|
className="bg-ink text-white px-4 py-2 rounded-sm hover:bg-gray-800 transition-colors"
|
|
>
|
|
{language === "en" ? "Hire Us" : "ติดต่อเรา"}
|
|
</Link>
|
|
|
|
{/* Language Toggle */}
|
|
<button
|
|
onClick={toggleLanguage}
|
|
className="flex items-center space-x-1 text-xs font-bold border border-gray-200 rounded-full px-3 py-1 hover:bg-gray-50 transition-colors"
|
|
>
|
|
<Globe size={14} className="text-subtle" />
|
|
<span className={language === "en" ? "text-ink" : "text-subtle"}>
|
|
EN
|
|
</span>
|
|
<span className="text-gray-300">|</span>
|
|
<span className={language === "th" ? "text-ink" : "text-subtle"}>
|
|
TH
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Mobile button */}
|
|
<div className="md:hidden flex items-center gap-4">
|
|
<button
|
|
onClick={toggleLanguage}
|
|
className="flex items-center space-x-1 text-xs font-bold border border-gray-200 rounded-full px-3 py-1"
|
|
>
|
|
<span className={language === "en" ? "text-ink" : "text-subtle"}>
|
|
EN
|
|
</span>
|
|
<span className="text-gray-300">|</span>
|
|
<span className={language === "th" ? "text-ink" : "text-subtle"}>
|
|
TH
|
|
</span>
|
|
</button>
|
|
<button onClick={() => setIsOpen(!isOpen)} className="text-ink p-2">
|
|
{isOpen ? <X size={24} /> : <Menu size={24} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Menu */}
|
|
{isOpen && (
|
|
<div className="md:hidden bg-white border-b border-gray-100">
|
|
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
|
<Link
|
|
to="/"
|
|
onClick={() => setIsOpen(false)}
|
|
className="block px-3 py-2 text-base font-medium text-ink"
|
|
>
|
|
Work
|
|
</Link>
|
|
<Link
|
|
to="/services"
|
|
onClick={() => setIsOpen(false)}
|
|
className="block px-3 py-2 text-base font-medium text-subtle"
|
|
>
|
|
Services
|
|
</Link>
|
|
<Link
|
|
to="/research"
|
|
onClick={() => setIsOpen(false)}
|
|
className="block px-3 py-2 text-base font-medium text-subtle"
|
|
>
|
|
Research
|
|
</Link>
|
|
<Link
|
|
to="/playground"
|
|
onClick={() => setIsOpen(false)}
|
|
className="block px-3 py-2 text-base font-medium text-subtle"
|
|
>
|
|
Playground
|
|
</Link>
|
|
<Link
|
|
to="/hire-us"
|
|
onClick={() => setIsOpen(false)}
|
|
className="block px-3 py-2 text-base font-medium text-ink font-bold"
|
|
>
|
|
Hire Us
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</nav>
|
|
);
|
|
};
|
|
|
|
const Footer: React.FC = () => {
|
|
const { language } = useLanguage();
|
|
return (
|
|
<footer className="bg-white border-t border-gray-100 py-12 mt-24">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 grid grid-cols-1 md:grid-cols-4 gap-8">
|
|
<div className="col-span-1 md:col-span-2">
|
|
<span className="font-sans tracking-widest text-xs font-bold uppercase text-ink">
|
|
Pradit
|
|
</span>
|
|
<p className="mt-4 text-subtle font-serif text-sm max-w-md">
|
|
{language === "en"
|
|
? "We build production-grade AI systems. No hype, just engineering. Specializing in LLM integration, custom evaluations, and scalable architecture."
|
|
: "เราสร้างระบบ AI ระดับใช้งานจริง เน้นวิศวกรรมไม่เน้นโฆษณา เชี่ยวชาญด้านการรวมระบบ LLM, การประเมินผล และสถาปัตยกรรมที่ปรับขนาดได้"}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-sans font-bold text-xs uppercase tracking-wider mb-4">
|
|
Offerings
|
|
</h3>
|
|
<ul className="space-y-2 text-sm text-subtle font-serif">
|
|
<li>
|
|
<Link
|
|
to="/services"
|
|
className="hover:text-ink hover:underline decoration-1 underline-offset-2"
|
|
>
|
|
MVP Development
|
|
</Link>
|
|
</li>
|
|
<li>
|
|
<Link
|
|
to="/services"
|
|
className="hover:text-ink hover:underline decoration-1 underline-offset-2"
|
|
>
|
|
System Audits
|
|
</Link>
|
|
</li>
|
|
<li>
|
|
<Link
|
|
to="/services"
|
|
className="hover:text-ink hover:underline decoration-1 underline-offset-2"
|
|
>
|
|
Custom Integration
|
|
</Link>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-sans font-bold text-xs uppercase tracking-wider mb-4">
|
|
Connect
|
|
</h3>
|
|
<ul className="space-y-2 text-sm text-subtle font-serif">
|
|
<li>
|
|
<a href="#" className="hover:text-ink">
|
|
GitHub
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<Link to="/playground" className="hover:text-ink">
|
|
Playground
|
|
</Link>
|
|
</li>
|
|
<li>
|
|
<Link to="/hire-us" className="hover:text-ink">
|
|
Hire Us
|
|
</Link>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
);
|
|
};
|
|
|
|
export default function App() {
|
|
const [language, setLanguage] = useState<Language>("en");
|
|
|
|
const toggleLanguage = () => {
|
|
setLanguage((prev) => (prev === "en" ? "th" : "en"));
|
|
};
|
|
|
|
return (
|
|
<LanguageContext.Provider value={{ language, toggleLanguage }}>
|
|
<HashRouter>
|
|
<div className="min-h-screen bg-paper flex flex-col relative">
|
|
<Navigation />
|
|
<main className="flex-grow">
|
|
<Routes>
|
|
<Route path="/" element={<Home />} />
|
|
<Route path="/research" element={<Research />} />
|
|
<Route
|
|
path="/research/lora-without-regret"
|
|
element={<BlogPost />}
|
|
/>
|
|
<Route path="/services" element={<Services />} />
|
|
<Route path="/playground" element={<Playground />} />
|
|
<Route path="/hire-us" element={<HireUs />} />
|
|
</Routes>
|
|
</main>
|
|
<Footer />
|
|
<ChatWidget />
|
|
</div>
|
|
</HashRouter>
|
|
</LanguageContext.Provider>
|
|
);
|
|
}
|