Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
494 changes: 444 additions & 50 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.2"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
Expand All @@ -23,6 +25,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"tailwindcss": "^4.2.2",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "npm:rolldown-vite@7.2.5"
Expand Down
163 changes: 158 additions & 5 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +1 to +6
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use dynamic viewport unit fallback for mobile viewport stability.

At Line 2, 100vh can cause visible jump/cropping on mobile browser UI show/hide. Add 100dvh fallback for more stable layout behavior.

📱 Proposed fix
 .app {
-  min-height: 100vh;
+  min-height: 100vh;
+  min-height: 100dvh;
   display: grid;
   place-items: center;
   padding: 1.5rem;
 }

As per coding guidelines, "Ensure that ... [the code] adheres to best practices recommended by lighthouse or similar tools for performance."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.app {
min-height: 100vh;
display: grid;
place-items: center;
padding: 1.5rem;
}
.app {
min-height: 100vh;
min-height: 100dvh;
display: grid;
place-items: center;
padding: 1.5rem;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.css` around lines 1 - 6, The .app selector uses min-height: 100vh
which can jump on mobile; update the .app rule (min-height) to provide a
dynamic-viewport fallback by adding a 100dvh declaration before the existing
100vh so browsers that support dynamic viewport use 100dvh while older browsers
fall back to 100vh (modify the min-height in the .app rule accordingly).


.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;
}
Comment on lines +28 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Standardize selector naming convention (camelCase vs kebab-case).

Lines 28, 35, 47, and 103 use camelCase selectors while other selectors are lowercase single-word. This breaks naming consistency and makes stylesheet evolution harder. Consider migrating to kebab-case with a compatibility bridge.

♻️ Proposed migration-safe refactor
-.searchForm {
+.searchForm,
+.search-form {
   margin-top: 1rem;
   display: flex;
   gap: 0.6rem;
   flex-wrap: wrap;
 }

-.srOnly {
+.srOnly,
+.sr-only {
   position: absolute;
   width: 1px;
   height: 1px;
   padding: 0;
   margin: -1px;
   overflow: hidden;
   clip-path: inset(50%);
   white-space: nowrap;
   border: 0;
 }

-.searchInput {
+.searchInput,
+.search-input {
   flex: 1;
   min-width: 220px;
   padding: 0.7rem 0.8rem;
   border-radius: 10px;
   border: 1px solid `#3b3b3b`;
   background: `#0f0f0f`;
   color: `#f3f3f3`;
 }

-.cardHeader {
+.cardHeader,
+.card-header {
   display: flex;
   gap: 0.85rem;
   align-items: center;
 }

As per coding guidelines, "Ensure that ... [the code] adheres to similar naming conventions for classes, ids."

Also applies to: 35-35, 47-47, 103-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.css` around lines 28 - 33, The CSS uses camelCase class selectors
(e.g., .searchForm and the other camelCase selectors at the referenced lines)
which breaks project naming consistency; rename these selectors to kebab-case
(e.g., .search-form) and provide a compatibility bridge by adding the original
camelCase selectors as aliases that reuse the new kebab-case rules (duplicate
the rules or use comma-separated selectors) so existing markup/components keep
working; after updating the CSS, search codebase for occurrences of the
camelCase class names (components, JSX/HTML) and update them to the new
kebab-case names or keep them until a follow-up refactor is scheduled.


.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;
}
136 changes: 130 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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<OrgResult | null>(null);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);

async function handleSearch(event: FormEvent<HTMLFormElement>) {
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 (
<>
<h1>Hello, OrgExplorer!</h1>
</>
)
<main className="app">
<section className="panel">
<h1 className="title">{uiText.appTitle}</h1>
<p className="subtitle">{uiText.appSubtitle}</p>

<form className="searchForm" onSubmit={handleSearch}>
<label htmlFor="org-login" className="srOnly">
{uiText.searchLabel}
</label>
<input
id="org-login"
className="searchInput"
placeholder={uiText.searchPlaceholder}
value={query}
onChange={(e) => setQuery(e.target.value)}
disabled={loading || refreshing}
/>
<button className="button primary" type="submit" disabled={loading || refreshing}>
{loading ? uiText.searchLoading : uiText.searchIdle}
</button>
</form>

{error && <p className="error">{error}</p>}

{result && (
<article className="card">
<div className="cardHeader">
<img className="avatar" src={result.org.avatar_url} alt="" />
<div>
<h2>{result.org.name || result.org.login}</h2>
<p className="handle">@{result.org.login}</p>
</div>
</div>

{result.org.description && <p className="description">{result.org.description}</p>}

<dl className="stats">
<div>
<dt>{uiText.publicRepos}</dt>
<dd>{result.org.public_repos.toLocaleString()}</dd>
</div>
<div>
<dt>{uiText.followers}</dt>
<dd>{result.org.followers.toLocaleString()}</dd>
</div>
<div>
<dt>{uiText.created}</dt>
<dd>{formatDate(result.org.created_at)}</dd>
</div>
<div>
<dt>{uiText.source}</dt>
<dd>{result.source === "cache" ? uiText.sourceCache : uiText.sourceApi}</dd>
</div>
</dl>

<p className="meta">
{uiText.lastFetched}: {new Date(result.cachedAt).toLocaleTimeString()}
</p>

<div className="actions">
<a className="button ghost" href={result.org.html_url} target="_blank" rel="noreferrer">
{uiText.openGitHub}
</a>
<button
className="button primary"
type="button"
onClick={handleRefresh}
disabled={loading || refreshing}
>
{refreshing ? uiText.refreshLoading : uiText.refreshIdle}
</button>
</div>
</article>
)}
</section>
</main>
);
}

export default App
export default App;
Loading
Loading