Skip to content

main#10

Open
it222 wants to merge 1 commit intoappwrite:mainfrom
it222:main
Open

main#10
it222 wants to merge 1 commit intoappwrite:mainfrom
it222:main

Conversation

@it222
Copy link

@it222 it222 commented Feb 26, 2026

What does this PR do?

(Provide a description of what this PR does.)

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)

Related PRs and Issues

(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)

Have you read the Contributing Guidelines on issues?

(Write your answer here.)

Summary by CodeRabbit

  • New Features

    • Added a blog/writeups section with Markdown post support
    • Added GitHub projects showcase with filtering and sorting
    • Added interactive threat model viewer for security scenarios
    • Added terminal-like command interface for navigation
    • Added threat radar widget with dynamic status visualization
    • Redesigned portfolio page with modular sections (Skills, Projects, Certs, Contact)
    • Added mouse-based parallax visual effects
  • Style

    • Applied comprehensive neon-themed visual design across the application

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Walkthrough

This PR transforms the application from a minimal interactive shell with Appwrite integration into a comprehensive neon-themed cyber portfolio page. Changes include: adding three new npm dependencies (lucide-react, marked, react-router-dom), replacing environment configuration with actual Appwrite credentials, rewriting the App component to use a modular section-based architecture with a terminal-like interface and scroll-reveal animations, introducing new components for GitHub projects display and threat modeling, implementing a Markdown-based blog system, and completely redesigning the stylesheet with neon/dark theme CSS and visual effects. The prior Tailwind imports and custom backgrounds have been removed in favor of a cohesive custom design system.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

πŸš₯ Pre-merge checks | βœ… 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'main' is vague and generic, failing to describe any meaningful aspect of the substantial changes (portfolio redesign, new components, styling, blog system). Replace with a descriptive title summarizing the main changes, such as 'Redesign portfolio with modular components, Terminal UI, and blog system' or 'Add portfolio page with interactive sections and new dependencies'.
βœ… Passed checks (1 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@it222
Copy link
Author

it222 commented Feb 26, 2026

main hi

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (3)
src/App.jsx (3)

257-262: Consider adding accessibility attributes to the terminal input.

The input lacks an accessible label. Screen reader users won't know the purpose of this field.

β™Ώ Proposed fix
         <input
           value={value}
           onChange={(e) => setValue(e.target.value)}
           placeholder="type a command…"
           spellCheck={false}
+          aria-label="Terminal command input"
         />
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.jsx` around lines 257 - 262, The terminal input element (the <input>
using value and onChange with setValue) is missing accessible labeling; add
either an explicit aria-label (e.g., aria-label="Terminal command input") or
associate a visible <label> with an id on the input (set id on the input and a
<label htmlFor="...">) so screen readers can identify its purpose; ensure any
placeholder remains but do not rely on it as the sole label and keep
spellCheck={false} as-is.

36-41: Use block body to avoid returning value from forEach callback.

The short-circuit expression returns a value to forEach, which ignores it. While functionally correct, using a block body is cleaner and silences the static analysis warning.

♻️ Proposed fix
     const io = new IntersectionObserver(
-      (entries) => entries.forEach((e) => e.isIntersecting && e.target.classList.add("is-visible")),
+      (entries) => {
+        entries.forEach((e) => {
+          if (e.isIntersecting) e.target.classList.add("is-visible");
+        });
+      },
       { threshold: 0.12 }
     );
-    els.forEach((el) => io.observe(el));
+    els.forEach((el) => { io.observe(el); });
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.jsx` around lines 36 - 41, The forEach callback currently uses a
short-circuit expression that returns a value (e.target.classList.add(...))
which forEach ignores; change the arrow callback for the IntersectionObserver
and the els.forEach to use block bodies and explicit if checks instead of && so
no value is returned to forEach. Locate the IntersectionObserver callback (the
entries => entries.forEach((e) => e.isIntersecting &&
e.target.classList.add("is-visible"))) and replace the inner arrow with a
block-bodied function that does: if (e.isIntersecting) {
e.target.classList.add("is-visible"); } and similarly ensure els.forEach((el) =>
io.observe(el)) remains unchanged except using a block body if your linter
prefers.

523-527: Form inputs should have name attributes.

The form inputs lack name attributes, which are essential for form data serialization when connecting to a real backend (EmailJS, Formspree, etc.).

♻️ Proposed fix
             <div className="row">
-              <input placeholder="Your name" required />
-              <input placeholder="Email" type="email" required />
+              <input name="name" placeholder="Your name" required />
+              <input name="email" placeholder="Email" type="email" required />
             </div>
-            <textarea placeholder="Message" rows={5} required />
+            <textarea name="message" placeholder="Message" rows={5} required />
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.jsx` around lines 523 - 527, The three form controls in src/App.jsx
(the two <input> elements and the <textarea> shown) are missing name attributes;
add name="name" to the first input, name="email" to the second input, and
name="message" to the textarea (or other backend-expected names) while keeping
existing props (placeholder, type, required, rows) so the form serializes
correctly for EmailJS/Formspree or other backends.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Around line 1-4: The .env.example contains real Appwrite values and incorrect
dotenv formatting; replace the concrete values for VITE_APPWRITE_PROJECT_ID,
VITE_APPWRITE_PROJECT_NAME, and VITE_APPWRITE_ENDPOINT with placeholder values
(e.g., VITE_APPWRITE_PROJECT_ID=YOUR_PROJECT_ID,
VITE_APPWRITE_PROJECT_NAME="YOUR_PROJECT_NAME",
VITE_APPWRITE_ENDPOINT=YOUR_ENDPOINT) and remove extra spaces around the equals
signs and stray quotes so each line follows valid dotenv syntax (no surrounding
spaces around '=' and use values or quoted placeholders consistently) to silence
linters and avoid leaking real credentials.

In `@src/App.jsx`:
- Around line 12-16: Update the placeholder contact links in the LINKS constant:
replace the generic LinkedIn URL and the example mailto with your actual
LinkedIn profile URL and a real contact email (or use an environment-driven
value) so that LINKS = [...] contains valid production contact targets; edit the
LINKS array in src/App.jsx (the LINKS constant) to point to your real
https://linkedin.com/in/your-profile and mailto:your@domain.com (or reference an
env var) and verify labels remain correct.

In `@src/blog/blogData.js`:
- Around line 67-68: Guard the comparator in posts.sort so invalid or missing
date strings don't produce NaN: inside the comparator used by posts.sort((a, b)
=> ...), parse each date with Date.parse or new Date(...).getTime(), check isNaN
and replace invalid values with a deterministic fallback (e.g., 0 or
Number.MIN_SAFE_INTEGER) before subtracting; optionally add a stable tie-breaker
(e.g., compare a.title or a.slug) when times are equal so ordering remains
deterministic for posts with identical/invalid dates.

In `@src/blog/BlogSection.jsx`:
- Line 50: The component is directly injecting current.html via
dangerouslySetInnerHTML (className "blogHtml"), creating an XSS sink; update
BlogSection.jsx to sanitize the HTML before rendering by passing current.html
through a trusted sanitizer (e.g., DOMPurify.sanitize or sanitize-html) and use
that sanitized string in dangerouslySetInnerHTML, handling null/undefined
current or html values and centralizing the sanitizer call (e.g., in the
BlogSection component or a utility like sanitizeHtmlForRender) so all rendered
post HTML passes through the sanitization boundary.

In `@src/GitHubProjects.jsx`:
- Around line 67-97: The component renders nothing when loading is false,
status.error is false, but repos is empty; update the render in
GitHubProjects.jsx to explicitly show an empty state when repos.length === 0 by
adding a conditional before calling repos.map (reference the repos array and the
existing status.loading/status.error checks) and return a clear message/card
(e.g., "No repositories found" or similar) with the same surrounding
markup/styling used for repo cards so users see an informative empty-state
instead of a blank area.

In `@src/styles.css`:
- Around line 562-573: The CSS contains two `@keyframes` named sweep causing the
latter to override the radar rotation; either merge the intended radar rotation
keyframe into a single `@keyframes` sweep or rename the radar-specific keyframes
(e.g., sweep -> radarSweepKeyframes) and update the .radarSweep rule to use the
new animation-name so the radar animation defined in .radarSweep uses the
correct keyframes; ensure only one `@keyframes` is present for that animation to
avoid accidental overrides.
- Line 35: The CSS violates stylelint rules: the font-family declaration
(font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial,
"Apple Color Emoji","Segoe UI Emoji";) must quote family names containing spaces
and ensure proper comma spacing, and the `@keyframes` names must match the
configured naming pattern; fix by quoting multi-word families like "Segoe UI"
and "Segoe UI Emoji" (and ensure spacing after commas), e.g., font-family:
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, "Apple Color
Emoji", "Segoe UI Emoji"; and rename any non-conforming `@keyframes` identifiers
to the allowed pattern (e.g., kebab-case) and update all
animation/animation-name usages to the new keyframe names (refer to the
font-family declaration and the `@keyframes` identifiers in the file).

In `@src/ThreatModel.jsx`:
- Around line 197-354: The component uses many CSS class selectors (e.g., .page,
.container, .tmHead, .tmFilters, .assetMap, .assetBtn, .threatCol,
.threatHeader, .legend, .sev, .threatCard, .threatList, .threatTop,
.threatTitleRow, .pillRow, .threatBody, .split, .block, .blockTitle, .tmNote,
.h1, .h3, .muted, .lead, .kicker, etc.) that are not defined, so add a
stylesheet with complete rules for these selectors (either extend src/styles.css
or create src/ThreatModel.css and import it in ThreatModel.jsx) to restore
layout and visuals; ensure responsive grid/flex rules for
.tmHead/.tmGrid/.threatCol, styles for Card children and buttons (.assetBtn
.active), severity legend (.sev.low/.sev.med/.sev.high), and utility text
classes (.muted .small .kicker .lead) so Score, Pill, and mapped lists render
correctly.

---

Nitpick comments:
In `@src/App.jsx`:
- Around line 257-262: The terminal input element (the <input> using value and
onChange with setValue) is missing accessible labeling; add either an explicit
aria-label (e.g., aria-label="Terminal command input") or associate a visible
<label> with an id on the input (set id on the input and a <label
htmlFor="...">) so screen readers can identify its purpose; ensure any
placeholder remains but do not rely on it as the sole label and keep
spellCheck={false} as-is.
- Around line 36-41: The forEach callback currently uses a short-circuit
expression that returns a value (e.target.classList.add(...)) which forEach
ignores; change the arrow callback for the IntersectionObserver and the
els.forEach to use block bodies and explicit if checks instead of && so no value
is returned to forEach. Locate the IntersectionObserver callback (the entries =>
entries.forEach((e) => e.isIntersecting &&
e.target.classList.add("is-visible"))) and replace the inner arrow with a
block-bodied function that does: if (e.isIntersecting) {
e.target.classList.add("is-visible"); } and similarly ensure els.forEach((el) =>
io.observe(el)) remains unchanged except using a block body if your linter
prefers.
- Around line 523-527: The three form controls in src/App.jsx (the two <input>
elements and the <textarea> shown) are missing name attributes; add name="name"
to the first input, name="email" to the second input, and name="message" to the
textarea (or other backend-expected names) while keeping existing props
(placeholder, type, required, rows) so the form serializes correctly for
EmailJS/Formspree or other backends.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 6100df3 and 59223e2.

β›” Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • src/assets/profile.jpg is excluded by !**/*.jpg
πŸ“’ Files selected for processing (11)
  • .env.example
  • package.json
  • src/App.css
  • src/App.jsx
  • src/GitHubProjects.jsx
  • src/ThreatModel.jsx
  • src/blog/BlogSection.jsx
  • src/blog/blogData.js
  • src/blog/posts/first-post.md
  • src/main.jsx
  • src/styles.css
πŸ’€ Files with no reviewable changes (1)
  • src/App.css

Comment on lines +1 to +4
VITE_APPWRITE_PROJECT_ID = "699ebd17003273658385"
VITE_APPWRITE_PROJECT_NAME = "kajan.meNew project"
VITE_APPWRITE_ENDPOINT = "https://fra.cloud.appwrite.io/v1"

No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace real environment values and fix dotenv formatting.

Line 1–3 should use placeholders in .env.example to avoid accidental coupling to a personal/shared Appwrite project, and the current formatting will keep linter warnings active.

πŸ”§ Proposed fix
-VITE_APPWRITE_PROJECT_ID = "699ebd17003273658385"
-VITE_APPWRITE_PROJECT_NAME = "kajan.meNew project"
-VITE_APPWRITE_ENDPOINT = "https://fra.cloud.appwrite.io/v1"
-        
+VITE_APPWRITE_ENDPOINT="https://<REGION>.cloud.appwrite.io/v1"
+VITE_APPWRITE_PROJECT_ID="<PROJECT_ID>"
+VITE_APPWRITE_PROJECT_NAME="<PROJECT_NAME>"
πŸ“ 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
VITE_APPWRITE_PROJECT_ID = "699ebd17003273658385"
VITE_APPWRITE_PROJECT_NAME = "kajan.meNew project"
VITE_APPWRITE_ENDPOINT = "https://fra.cloud.appwrite.io/v1"
VITE_APPWRITE_ENDPOINT="https://<REGION>.cloud.appwrite.io/v1"
VITE_APPWRITE_PROJECT_ID="<PROJECT_ID>"
VITE_APPWRITE_PROJECT_NAME="<PROJECT_NAME>"
🧰 Tools
πŸͺ› dotenv-linter (4.0.0)

[warning] 1-1: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 2-2: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 3-3: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 3-3: [UnorderedKey] The VITE_APPWRITE_ENDPOINT key should go before the VITE_APPWRITE_PROJECT_ID key

(UnorderedKey)


[warning] 4-4: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)


[warning] 4-4: [TrailingWhitespace] Trailing whitespace detected

(TrailingWhitespace)

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 1 - 4, The .env.example contains real Appwrite
values and incorrect dotenv formatting; replace the concrete values for
VITE_APPWRITE_PROJECT_ID, VITE_APPWRITE_PROJECT_NAME, and VITE_APPWRITE_ENDPOINT
with placeholder values (e.g., VITE_APPWRITE_PROJECT_ID=YOUR_PROJECT_ID,
VITE_APPWRITE_PROJECT_NAME="YOUR_PROJECT_NAME",
VITE_APPWRITE_ENDPOINT=YOUR_ENDPOINT) and remove extra spaces around the equals
signs and stray quotes so each line follows valid dotenv syntax (no surrounding
spaces around '=' and use values or quoted placeholders consistently) to silence
linters and avoid leaking real credentials.

Comment on lines +12 to +16
const LINKS = [
{ label: "GitHub", href: "https://github.com/sksivakajan" },
{ label: "LinkedIn", href: "https://linkedin.com/" },
{ label: "Email", href: "mailto:you@example.com" },
];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Placeholder contact URLs should be updated.

The LINKS array contains placeholder values (https://linkedin.com/ and mailto:you@example.com) that should be replaced with actual contact information before deploying to production.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.jsx` around lines 12 - 16, Update the placeholder contact links in
the LINKS constant: replace the generic LinkedIn URL and the example mailto with
your actual LinkedIn profile URL and a real contact email (or use an
environment-driven value) so that LINKS = [...] contains valid production
contact targets; edit the LINKS array in src/App.jsx (the LINKS constant) to
point to your real https://linkedin.com/in/your-profile and
mailto:your@domain.com (or reference an env var) and verify labels remain
correct.

Comment on lines +67 to +68
posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
return posts;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Guard sorting against invalid/missing date values.

Line 67 can return NaN for empty/invalid dates, which makes ordering non-deterministic for those posts.

βœ… Proposed fix
-  posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+  const toTs = (d) => {
+    const ts = Date.parse(d);
+    return Number.isNaN(ts) ? 0 : ts;
+  };
+  posts.sort((a, b) => toTs(b.date) - toTs(a.date));
   return posts;
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/blog/blogData.js` around lines 67 - 68, Guard the comparator in
posts.sort so invalid or missing date strings don't produce NaN: inside the
comparator used by posts.sort((a, b) => ...), parse each date with Date.parse or
new Date(...).getTime(), check isNaN and replace invalid values with a
deterministic fallback (e.g., 0 or Number.MIN_SAFE_INTEGER) before subtracting;
optionally add a stable tie-breaker (e.g., compare a.title or a.slug) when times
are equal so ordering remains deterministic for posts with identical/invalid
dates.

</div>

{current ? (
<div className="blogHtml" dangerouslySetInnerHTML={{ __html: current.html }} />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid rendering post HTML via dangerouslySetInnerHTML without a sanitization boundary.

Line 50 injects HTML directly into the DOM, which creates an XSS sink if any markdown content is compromised or later sourced dynamically.

🧰 Tools
πŸͺ› Biome (2.4.4)

[error] 50-50: Avoid passing content using the dangerouslySetInnerHTML prop.

(lint/security/noDangerouslySetInnerHtml)

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/blog/BlogSection.jsx` at line 50, The component is directly injecting
current.html via dangerouslySetInnerHTML (className "blogHtml"), creating an XSS
sink; update BlogSection.jsx to sanitize the HTML before rendering by passing
current.html through a trusted sanitizer (e.g., DOMPurify.sanitize or
sanitize-html) and use that sanitized string in dangerouslySetInnerHTML,
handling null/undefined current or html values and centralizing the sanitizer
call (e.g., in the BlogSection component or a utility like
sanitizeHtmlForRender) so all rendered post HTML passes through the sanitization
boundary.

Comment on lines +67 to +97
{!status.loading &&
!status.error &&
repos.map((r) => (
<article className="card proj" key={r.id}>
<div className="projTop">
<div className="projTitle">{r.name}</div>
<div className="projBadge">β˜… {r.stargazers_count ?? 0}</div>
</div>

<p className="projDesc">{r.description || "No description provided."}</p>

<div className="tagRow">
{r.language ? <span className="tag">{r.language}</span> : null}
<span className="tag">
Updated: {new Date(r.updated_at).toLocaleDateString()}
</span>
</div>

<div className="projActions">
<a className="btn mini" href={r.html_url} target="_blank" rel="noreferrer">
View Repo
</a>
{r.homepage ? (
<a className="btn mini ghost" href={r.homepage} target="_blank" rel="noreferrer">
Live
</a>
) : null}
</div>
</article>
))}
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Add an explicit empty state for zero repositories.

When loading completes successfully but repos.length === 0, users currently see a blank section instead of a clear message.

πŸ’‘ Proposed fix
       {!status.loading &&
         !status.error &&
+        repos.length === 0 && (
+          <div className="card">
+            <div className="cardTitle">No projects found</div>
+            <p className="cardBody">This account has no public non-fork repositories yet.</p>
+          </div>
+        )}
+
+      {!status.loading &&
+        !status.error &&
+        repos.length > 0 &&
         repos.map((r) => (
           <article className="card proj" key={r.id}>
πŸ“ 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
{!status.loading &&
!status.error &&
repos.map((r) => (
<article className="card proj" key={r.id}>
<div className="projTop">
<div className="projTitle">{r.name}</div>
<div className="projBadge">β˜… {r.stargazers_count ?? 0}</div>
</div>
<p className="projDesc">{r.description || "No description provided."}</p>
<div className="tagRow">
{r.language ? <span className="tag">{r.language}</span> : null}
<span className="tag">
Updated: {new Date(r.updated_at).toLocaleDateString()}
</span>
</div>
<div className="projActions">
<a className="btn mini" href={r.html_url} target="_blank" rel="noreferrer">
View Repo
</a>
{r.homepage ? (
<a className="btn mini ghost" href={r.homepage} target="_blank" rel="noreferrer">
Live
</a>
) : null}
</div>
</article>
))}
</div>
{!status.loading &&
!status.error &&
repos.length === 0 && (
<div className="card">
<div className="cardTitle">No projects found</div>
<p className="cardBody">This account has no public non-fork repositories yet.</p>
</div>
)}
{!status.loading &&
!status.error &&
repos.length > 0 &&
repos.map((r) => (
<article className="card proj" key={r.id}>
<div className="projTop">
<div className="projTitle">{r.name}</div>
<div className="projBadge">β˜… {r.stargazers_count ?? 0}</div>
</div>
<p className="projDesc">{r.description || "No description provided."}</p>
<div className="tagRow">
{r.language ? <span className="tag">{r.language}</span> : null}
<span className="tag">
Updated: {new Date(r.updated_at).toLocaleDateString()}
</span>
</div>
<div className="projActions">
<a className="btn mini" href={r.html_url} target="_blank" rel="noreferrer">
View Repo
</a>
{r.homepage ? (
<a className="btn mini ghost" href={r.homepage} target="_blank" rel="noreferrer">
Live
</a>
) : null}
</div>
</article>
))}
</div>
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/GitHubProjects.jsx` around lines 67 - 97, The component renders nothing
when loading is false, status.error is false, but repos is empty; update the
render in GitHubProjects.jsx to explicitly show an empty state when repos.length
=== 0 by adding a conditional before calling repos.map (reference the repos
array and the existing status.loading/status.error checks) and return a clear
message/card (e.g., "No repositories found" or similar) with the same
surrounding markup/styling used for repo cards so users see an informative
empty-state instead of a blank area.

html,body{ height:100%; }
body{
margin:0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, "Apple Color Emoji","Segoe UI Emoji";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Resolve stylelint rule violations (font-family-name-quotes, keyframes-name-pattern).

Line 35 and Line 63 currently violate configured lint rules, which can fail style checks.

🧹 Proposed fix
-  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, "Apple Color Emoji","Segoe UI Emoji";
+  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, "Apple Color Emoji","Segoe UI Emoji";
@@
-  animation: gridFloat 7s ease-in-out infinite;
+  animation: grid-float 7s ease-in-out infinite;
 }
-@keyframes gridFloat{
+@keyframes grid-float{
   0%,100%{ transform: perspective(900px) rotateX(55deg) translateY(-220px); }
   50%{ transform: perspective(900px) rotateX(55deg) translateY(-200px); }
 }

Also applies to: 61-64

🧰 Tools
πŸͺ› Stylelint (17.3.0)

[error] 35-35: Expected quotes around "Segoe UI" (font-family-name-quotes)

(font-family-name-quotes)

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/styles.css` at line 35, The CSS violates stylelint rules: the font-family
declaration (font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI,
Roboto, Arial, "Apple Color Emoji","Segoe UI Emoji";) must quote family names
containing spaces and ensure proper comma spacing, and the `@keyframes` names must
match the configured naming pattern; fix by quoting multi-word families like
"Segoe UI" and "Segoe UI Emoji" (and ensure spacing after commas), e.g.,
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial,
"Apple Color Emoji", "Segoe UI Emoji"; and rename any non-conforming `@keyframes`
identifiers to the allowed pattern (e.g., kebab-case) and update all
animation/animation-name usages to the new keyframe names (refer to the
font-family declaration and the `@keyframes` identifiers in the file).

Comment on lines +562 to +573
.radarSweep{
position:absolute;
inset:-50%;
background: conic-gradient(from 0deg, rgba(0,255,156,.0), rgba(0,255,156,.0), rgba(0,255,156,.18), rgba(0,255,156,.0));
animation: sweep 2.7s linear infinite;
transform-origin: 50% 50%;
mix-blend-mode: screen;
}
@keyframes sweep{
0%{ transform: rotate(0deg); }
100%{ transform: rotate(360deg); }
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

@keyframes sweep is declared twice; the second declaration overrides the first animation.

The later definition (Line 903) replaces the radar rotation keyframes (Line 570), so .radarSweep animation behavior is unintentionally changed.

πŸ› οΈ Proposed fix
 .scanSweep{
   opacity: .22;
   filter: blur(10px);
   mix-blend-mode: screen;
-  animation: sweep 2.6s linear infinite;
+  animation: sweep-x 2.6s linear infinite;
   pointer-events: none;
 }
@@
-@keyframes sweep{
+@keyframes sweep-x{
   0%{ transform: translateX(-80px) }
   100%{ transform: translateX(680px) }
 }

Also applies to: 879-906

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/styles.css` around lines 562 - 573, The CSS contains two `@keyframes` named
sweep causing the latter to override the radar rotation; either merge the
intended radar rotation keyframe into a single `@keyframes` sweep or rename the
radar-specific keyframes (e.g., sweep -> radarSweepKeyframes) and update the
.radarSweep rule to use the new animation-name so the radar animation defined in
.radarSweep uses the correct keyframes; ensure only one `@keyframes` is present
for that animation to avoid accidental overrides.

Comment on lines +197 to +354
<div className="page">
<header className="nav">
<div className="navInner">
<Link className="btn" to="/">
<ArrowLeft size={16} /> Back
</Link>

<div className="brandInline">
<Target size={18} />
<span className="brandText">Threat Model</span>
</div>

<div />
</div>
</header>

<main className="container">
<div className="tmHead">
<div>
<div className="kicker">Interactive</div>
<h1 className="h1">{model.title}</h1>
<p className="muted lead">{model.subtitle}</p>
</div>

<Card className="tmFilters">
<div className="filterTop">
<Filter size={16} />
<div className="h3">Filters</div>
</div>

<div className="filterGrid">
<label className="field">
<span className="muted small">Minimum Severity</span>
<select value={minSev} onChange={(e) => setMinSev(e.target.value)}>
<option value="LOW">LOW+</option>
<option value="MED">MED+</option>
<option value="HIGH">HIGH</option>
</select>
</label>

<label className="field">
<span className="muted small">STRIDE</span>
<select value={stride} onChange={(e) => setStride(e.target.value)}>
{strideOptions.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
</label>
</div>
</Card>
</div>

<div className="tmGrid">
{/* Asset map */}
<Card className="assetMap">
<div className="assetHeader">
<div className="h3">Assets</div>
<div className="muted small">Click an asset</div>
</div>

<div className="assetList">
{model.assets.map((a) => (
<button
key={a.id}
className={cx("assetBtn", a.id === activeAsset && "active")}
onClick={() => setActiveAsset(a.id)}
type="button"
>
<div className="assetName">{a.name}</div>
<div className="muted small">{a.type}</div>
</button>
))}
</div>

<div className="assetFooter">
<div className="muted small">Selected</div>
<div className="assetSelected">{activeAssetObj?.name}</div>
</div>
</Card>

{/* Threats */}
<div className="threatCol">
<Card className="threatHeader">
<div>
<div className="kicker">Threats</div>
<div className="h3">{activeAssetObj?.name}</div>
<div className="muted small">{filtered.length} item(s) match filters</div>
</div>
<div className="legend">
<span className="sev low">LOW</span>
<span className="sev med">MED</span>
<span className="sev high">HIGH</span>
</div>
</Card>

<div className="threatList">
{filtered.map((t) => (
<Card key={t.id} className="threatCard">
<div className="threatTop">
<div className="threatTitleRow">
<div className="h3">{t.title}</div>
<Score likelihood={t.likelihood} impact={t.impact} />
</div>

<div className="pillRow">
<Pill>{t.stride}</Pill>
<Pill>Asset: {activeAssetObj?.type}</Pill>
</div>
</div>

<div className="threatBody">
<div className="split">
<div className="block">
<div className="blockTitle">
<ShieldAlert size={16} /> Evidence / Concern
</div>
<p className="muted p">{t.evidence}</p>
</div>

<div className="block">
<div className="blockTitle">
<ShieldCheck size={16} /> Mitigations
</div>
<ul className="list">
{t.mitigations.map((m) => (
<li key={m} className="muted">
{m}
</li>
))}
</ul>
</div>
</div>
</div>
</Card>
))}

{!filtered.length && (
<Card className="threatCard">
<div className="h3">No threats match your filters</div>
<p className="muted p">Try lowering the minimum severity or changing STRIDE filter.</p>
</Card>
)}
</div>
</div>
</div>

<Card className="tmNote">
<div className="h3">How to use this in interviews</div>
<p className="muted p">
Tell them: β€œI structure projects with assets, threats (STRIDE), likelihood/impact scoring,
and mitigations β€” the same way real teams do threat modeling.”
</p>
</Card>
</main>
</div>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all CSS files first
echo "=== CSS Files in repo ==="
find . -name "*.css" -type f | head -20

echo ""
echo "=== Checking ThreatModel class selectors ==="

classes=(
  page container brandInline tmHead tmFilters filterTop filterGrid field
  tmGrid assetMap assetHeader assetList assetBtn active assetName assetFooter assetSelected
  threatCol threatHeader legend sev score scoreNum threatList threatCard threatTop threatTitleRow
  pillRow threatBody split block blockTitle list tmNote h1 h3 muted lead small kicker
)

missing=0
found=0

for c in "${classes[@]}"; do
  if rg -l --type=css "\.${c}\b" 2>/dev/null | head -1 | grep -q .; then
    found=$((found + 1))
  else
    echo "MISSING .$c"
    missing=$((missing + 1))
  fi
done

echo ""
echo "Found: $found, Missing: $missing"

Repository: appwrite/starter-for-react

Length of output: 882


🏁 Script executed:

#!/bin/bash
# Check what CSS is actually in styles.css
echo "=== Content of styles.css ==="
wc -l src/styles.css
echo ""
head -100 src/styles.css

Repository: appwrite/starter-for-react

Length of output: 3368


🏁 Script executed:

#!/bin/bash
# Search for specific ThreatModel-related selectors in styles.css
echo "=== Searching for specific selectors in styles.css ==="
echo ""
echo "Page/container layout selectors:"
rg -n "^\.(page|container|nav|main)" src/styles.css

echo ""
echo "ThreatModel specific selectors (tmHead, assetMap, threatCard, etc):"
rg -n "^\.(tmHead|tmFilters|assetMap|assetBtn|threatCard|threatCol)" src/styles.css

echo ""
echo "Utility selectors (h1, h3, muted, etc):"
rg -n "^\.(h1|h3|h2|muted|lead|small|kicker)" src/styles.css

Repository: appwrite/starter-for-react

Length of output: 348


🏁 Script executed:

#!/bin/bash
# Get all CSS selectors that are defined in styles.css
echo "=== All class selectors defined in styles.css ==="
rg "^\.[a-zA-Z0-9_-]+" src/styles.css -o | sort -u | head -50

Repository: appwrite/starter-for-react

Length of output: 567


🏁 Script executed:

#!/bin/bash
# Get ALL class selectors from styles.css
echo "=== All class selectors in styles.css ==="
rg "^\.[a-zA-Z0-9_-]+" src/styles.css -o | sort -u

echo ""
echo "=== Classes used in ThreatModel.jsx that ARE defined ==="
tm_classes=(
  "page" "container" "brandInline" "tmHead" "tmFilters" "filterTop" "filterGrid" "field"
  "tmGrid" "assetMap" "assetHeader" "assetList" "assetBtn" "active" "assetName" "assetFooter" "assetSelected"
  "threatCol" "threatHeader" "legend" "sev" "score" "scoreNum" "threatList" "threatCard" "threatTop" "threatTitleRow"
  "pillRow" "threatBody" "split" "block" "blockTitle" "list" "tmNote" "h1" "h3" "muted" "lead" "small" "kicker"
)

all_selectors=$(rg "^\.[a-zA-Z0-9_-]+" src/styles.css -o | sed 's/^\.//' | sort -u)

found=0
missing=0
for c in "${tm_classes[@]}"; do
  if echo "$all_selectors" | grep -q "^${c}$"; then
    echo "FOUND: .$c"
    found=$((found + 1))
  else
    missing=$((missing + 1))
  fi
done

echo ""
echo "Total found: $found, Missing: $missing"

Repository: appwrite/starter-for-react

Length of output: 1241


All 40 ThreatModel CSS selectors are missing from styles.css β€” the component will render but be completely unstyled.

The ThreatModel.jsx component uses class names for layout and styling that do not exist in the codebase: .page, .container, .tmHead, .tmFilters, .assetMap, .assetBtn, .threatCard, .threatCol, .threatHeader, .legend, .sev, .score, .scoreNum, .threatList, .threatTop, .threatTitleRow, .pillRow, .threatBody, .split, .block, .blockTitle, .list, .tmNote, .h1, .h3, .muted, .lead, .small, .kicker, and others. None of these selectors are defined in src/styles.css. The component must have complete CSS definitions added to src/styles.css or moved to a dedicated stylesheet before it will render correctly.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ThreatModel.jsx` around lines 197 - 354, The component uses many CSS
class selectors (e.g., .page, .container, .tmHead, .tmFilters, .assetMap,
.assetBtn, .threatCol, .threatHeader, .legend, .sev, .threatCard, .threatList,
.threatTop, .threatTitleRow, .pillRow, .threatBody, .split, .block, .blockTitle,
.tmNote, .h1, .h3, .muted, .lead, .kicker, etc.) that are not defined, so add a
stylesheet with complete rules for these selectors (either extend src/styles.css
or create src/ThreatModel.css and import it in ThreatModel.jsx) to restore
layout and visuals; ensure responsive grid/flex rules for
.tmHead/.tmGrid/.threatCol, styles for Card children and buttons (.assetBtn
.active), severity legend (.sev.low/.sev.med/.sev.high), and utility text
classes (.muted .small .kicker .lead) so Score, Pill, and mapped lists render
correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants