diff --git a/package.json b/package.json deleted file mode 100644 index d75669c..0000000 --- a/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "OrgExplorer", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "react": "^19.2.0", - "react-dom": "^19.2.0" - }, - "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/node": "^24.10.1", - "@types/react": "^19.2.5", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "typescript": "~5.9.3", - "typescript-eslint": "^8.46.4", - "vite": "npm:rolldown-vite@7.2.5" - }, - "overrides": { - "vite": "npm:rolldown-vite@7.2.5" - } -} diff --git a/src/App.css b/src/App.css index 027945e..e62f472 100644 --- a/src/App.css +++ b/src/App.css @@ -1,6 +1,159 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; +.app { + min-height: 100vh; + display: grid; + place-items: center; + padding: 1.5rem; +} + +.panel { + width: 100%; + max-width: 760px; + border: 1px solid #2a2a2a; + border-radius: 16px; + background: #171717; + box-shadow: 0 12px 38px rgba(0, 0, 0, 0.32); + padding: 1.25rem; +} + +.title { + margin: 0; + font-size: 1.8rem; +} + +.subtitle { + color: #a0a0a0; + margin-top: 0.4rem; +} + +.searchForm { + margin-top: 1rem; + display: flex; + gap: 0.6rem; + flex-wrap: wrap; +} + +.srOnly { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + +.searchInput { + flex: 1; + min-width: 220px; + padding: 0.7rem 0.8rem; + border-radius: 10px; + border: 1px solid #3b3b3b; + background: #0f0f0f; + color: #f3f3f3; +} + +.searchInput:focus { + outline: 2px solid #60a5fa; + outline-offset: 1px; +} + +.button { + border: 0; + border-radius: 10px; + padding: 0.7rem 0.9rem; + font-weight: 600; + cursor: pointer; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.button:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.button.primary { + background: #2563eb; + color: #fff; +} + +.button.ghost { + background: #0f0f0f; + color: #e8e8e8; + border: 1px solid #3b3b3b; +} + +.error { + margin-top: 0.8rem; + color: #fca5a5; +} + +.card { + margin-top: 1rem; + border: 1px solid #2f2f2f; + border-radius: 14px; + padding: 1rem; + background: #111111; +} + +.cardHeader { + display: flex; + gap: 0.85rem; + align-items: center; +} + +.avatar { + width: 64px; + height: 64px; + border-radius: 12px; +} + +.handle { + margin: 0.15rem 0 0; + color: #a0a0a0; +} + +.description { + margin: 0.9rem 0; + color: #d6d6d6; +} + +.stats { + margin: 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 0.7rem; +} + +.stats div { + padding: 0.55rem; + border: 1px solid #2d2d2d; + border-radius: 10px; +} + +.stats dt { + color: #9f9f9f; + font-size: 0.82rem; +} + +.stats dd { + margin: 0.2rem 0 0; + font-weight: 600; +} + +.meta { + margin-top: 0.8rem; + color: #9f9f9f; + font-size: 0.88rem; +} + +.actions { + margin-top: 0.9rem; + display: flex; + gap: 0.6rem; + flex-wrap: wrap; } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 0a3deb1..f3bc637 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,136 @@ -import './App.css' +import { useState, type FormEvent } from "react"; +import "./App.css"; +import { getOrganization, refreshOrganization } from "./cache/orgCache"; +import type { OrgResult } from "./api/github"; +import { uiText } from "./i18n/strings"; + +function formatDate(iso: string): string { + try { + return new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }).format(new Date(iso)); + } catch { + return iso; + } +} function App() { + const [query, setQuery] = useState(""); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + + async function handleSearch(event: FormEvent) { + event.preventDefault(); + setError(null); + setLoading(true); + + try { + const org = await getOrganization(query); + setResult(org); + } catch (err) { + setResult(null); + setError(err instanceof Error ? err.message : uiText.fetchErrorFallback); + } finally { + setLoading(false); + } + } + + async function handleRefresh() { + if (!result) return; + setRefreshing(true); + setError(null); + try { + const refreshed = await refreshOrganization(result.org.login); + setResult(refreshed); + } catch (err) { + setError(err instanceof Error ? err.message : uiText.refreshErrorFallback); + } finally { + setRefreshing(false); + } + } return ( - <> -

Hello, OrgExplorer!

- - ) +
+
+

{uiText.appTitle}

+

{uiText.appSubtitle}

+ +
+ + setQuery(e.target.value)} + disabled={loading || refreshing} + /> + +
+ + {error &&

{error}

} + + {result && ( +
+
+ +
+

{result.org.name || result.org.login}

+

@{result.org.login}

+
+
+ + {result.org.description &&

{result.org.description}

} + +
+
+
{uiText.publicRepos}
+
{result.org.public_repos.toLocaleString()}
+
+
+
{uiText.followers}
+
{result.org.followers.toLocaleString()}
+
+
+
{uiText.created}
+
{formatDate(result.org.created_at)}
+
+
+
{uiText.source}
+
{result.source === "cache" ? uiText.sourceCache : uiText.sourceApi}
+
+
+ +

+ {uiText.lastFetched}: {new Date(result.cachedAt).toLocaleTimeString()} +

+ +
+ + {uiText.openGitHub} + + +
+
+ )} +
+
+ ); } -export default App +export default App; diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..4ff4f8f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [react(), tailwindcss()], +});