Skip to content

Projects Page Search UX Improvements#1133

Open
matmanna wants to merge 12 commits into
hackclub:mainfrom
matmanna:projects-page
Open

Projects Page Search UX Improvements#1133
matmanna wants to merge 12 commits into
hackclub:mainfrom
matmanna:projects-page

Conversation

@matmanna
Copy link
Copy Markdown
Member

@matmanna matmanna commented Apr 5, 2026

Some of the Hackatime improvements I've wanted for a while :D

image image image

@matmanna matmanna marked this pull request as ready for review May 2, 2026 20:28
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 2, 2026

Greptile Summary

This PR adds several UX improvements to the Projects page: a client-side text search box, an archival-status selector (active/archived), a sort dropdown, and a grid/list view toggle. The broken-name warning is also now surfaced in list view as a compact badge.

  • Client-side filteredAndSortedProjects derived state powers search and sort without additional server requests; the archival-status change still triggers a full page reload via window.location.href to stay consistent with the existing URL-driven pattern.
  • The complete project action-button set (homepage, GitHub, edit, archive/restore) is duplicated across the list-mode header block and the grid-mode action bar, roughly doubling the template lines needed to maintain those controls.

Confidence Score: 5/5

Safe to merge; all changes are client-side rendering improvements with no impact on data persistence or security boundaries.

The change is entirely within a single Svelte component and touches only how project data is displayed and filtered. The derived filtering and sorting operate on already-loaded data, the archival status redirects use the existing URL-building helpers, and no new server endpoints are introduced.

app/javascript/pages/Projects/Index.svelte — specifically the duplicated action-button blocks for list vs. grid view, which will need to stay in sync manually until extracted into a component.

Important Files Changed

Filename Overview
app/javascript/pages/Projects/Index.svelte Adds client-side search, sort, archival-status filter, and grid/list view toggle to the Projects page. Action buttons are duplicated across list and grid render paths (~60 lines), and min-h-36 is applied in list mode making rows unnecessarily tall; both are style-level issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Projects page loads] --> B{projects_data deferred?}
    B -- loading --> C[Skeleton cards]
    B -- ready --> D{projects_data.projects.length == 0?}
    D -- yes --> E[Server empty state message]
    D -- no --> F[filteredAndSortedProjects derived]
    F --> G{User inputs}
    G -- searchQuery change --> F
    G -- sortBy change --> F
    G -- archivalStatus change --> H[changeArchivedStatus]
    H --> I[window.location.href = new URL]
    I --> A
    F --> J{filteredAndSortedProjects.length == 0?}
    J -- yes --> K[No search results message]
    J -- no --> L{viewMode}
    L -- grid --> M[Auto-fill grid of cards]
    L -- list --> N[Compact list rows]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
app/javascript/pages/Projects/Index.svelte:603-650
**Action buttons duplicated across list and grid modes**

The complete set of project action buttons — homepage link, GitHub link, edit/link-repo button, and archive/restore button — is rendered twice: once in the list-mode header block (here) and again in the grid-mode action bar (~lines 683–735). That's ~60 lines of near-identical markup that diverges over time. The repository guidelines suggest extracting duplicated markup into a shared component; a small `ProjectActions.svelte` accepting `project`, `show_archived`, and `viewMode` as props would serve both modes without the duplication.

### Issue 2 of 2
app/javascript/pages/Projects/Index.svelte:564-570
The `min-h-36` class (144 px) is applied unconditionally in both grid and list modes. In list mode this makes every row at least 144 px tall, producing a sparse list that looks more like a short grid than a compact view. List rows need far less vertical space — removing the minimum height in list mode lets the content dictate the row height naturally.

```suggestion
                <article
                  class="group relative flex w-full {viewMode === 'list'
                    ? 'flex-row items-start sm:items-center sm:justify-between'
                    : 'min-h-36'} overflow-hidden rounded-2xl {projectHref
                    ? 'cursor-pointer'
                    : ''}"
                >
```

Reviews (3): Last reviewed commit: "chore: format" | Re-trigger Greptile

Comment on lines +163 to +168
onMount(() => {
csrfToken =
document
.querySelector("meta[name='csrf-token']")
?.getAttribute("content") || "";
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Manual CSRF token replaces Inertia form utilities

This PR removes the Form import from @inertiajs/svelte and instead manually reads the CSRF token from the DOM via onMount, then injects it as a hidden authenticity_token input in a raw HTML <form>. This bypasses Inertia's built-in CSRF protection, validation state, and error handling. The preferred pattern is to use router.patch (already used in confirmStatusChange) or Inertia's useForm, both of which handle CSRF automatically.

Rule Used: What: Always use Inertia's <Form> or useForm u... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 163-168

Comment:
**Manual CSRF token replaces Inertia form utilities**

This PR removes the `Form` import from `@inertiajs/svelte` and instead manually reads the CSRF token from the DOM via `onMount`, then injects it as a hidden `authenticity_token` input in a raw HTML `<form>`. This bypasses Inertia's built-in CSRF protection, validation state, and error handling. The preferred pattern is to use `router.patch` (already used in `confirmStatusChange`) or Inertia's `useForm`, both of which handle CSRF automatically.

**Rule Used:** What: Always use Inertia's `<Form>` or `useForm` u... ([source](https://app.greptile.com/review/custom-context?memory=03426195-806a-4980-ad3d-f1c3db3c22fb))

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +592 to +610
<form method="post" action={project.update_path} class="space-y-3">
<input type="hidden" name="_method" value="patch" />
<input type="hidden" name="authenticity_token" value={csrfToken} />

<input
type="url"
name="project_repo_mapping[repo_url]"
bind:value={repoUrlDraft}
placeholder="https://github.com/owner/repo"
class="w-full rounded-lg border border-surface-200 bg-darker px-3 py-2 text-sm text-surface-content focus:border-primary focus:outline-none"
/>

<div class="flex gap-2">
<Button type="submit" variant="primary" size="sm" class="flex-1">Save</Button>
<Button type="button" variant="dark" size="sm" class="flex-1" onclick={closeMappingEditor}>
Cancel
</Button>
</div>
</form>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Raw HTML form with manual CSRF injection

The mapping editor form uses a raw <form method="post"> with a hidden authenticity_token input populated from the manually fetched csrfToken. Because csrfToken is initialised to "" and only populated in onMount, there's a window where a rapid form submission could send an empty token. Using router.patch or Inertia's useForm — consistent with confirmStatusChange — would eliminate this race and remove the manual CSRF plumbing entirely.

Rule Used: What: Always use Inertia's <Form> or useForm u... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 592-610

Comment:
**Raw HTML form with manual CSRF injection**

The mapping editor form uses a raw `<form method="post">` with a hidden `authenticity_token` input populated from the manually fetched `csrfToken`. Because `csrfToken` is initialised to `""` and only populated in `onMount`, there's a window where a rapid form submission could send an empty token. Using `router.patch` or Inertia's `useForm` — consistent with `confirmStatusChange` — would eliminate this race and remove the manual CSRF plumbing entirely.

**Rule Used:** What: Always use Inertia's `<Form>` or `useForm` u... ([source](https://app.greptile.com/review/custom-context?memory=03426195-806a-4980-ad3d-f1c3db3c22fb))

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +152 to +154
$effect(() => {
archivalStatus = show_archived ? "archived" : "active";
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 $effect used to derive state from a prop

This $effect sets archivalStatus from show_archived, which is purely derived state and doesn't need an effect. Since changeArchivedStatus uses window.location.href (a full page reload), show_archived will always be fresh on mount. archivalStatus can simply be initialized as $state(show_archived ? "archived" : "active"), removing one effect and reducing unnecessary re-runs.

Rule Used: What: Don't use effects to derive state that can b... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 152-154

Comment:
**`$effect` used to derive state from a prop**

This `$effect` sets `archivalStatus` from `show_archived`, which is purely derived state and doesn't need an effect. Since `changeArchivedStatus` uses `window.location.href` (a full page reload), `show_archived` will always be fresh on mount. `archivalStatus` can simply be initialized as `$state(show_archived ? "archived" : "active")`, removing one effect and reducing unnecessary re-runs.

**Rule Used:** What: Don't use effects to derive state that can b... ([source](https://app.greptile.com/review/custom-context?memory=8569b05b-35ae-4e04-9b00-50fe1f992c68))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@matmanna matmanna marked this pull request as draft May 8, 2026 11:51
@matmanna matmanna marked this pull request as ready for review May 21, 2026 03:04
@matmanna
Copy link
Copy Markdown
Member Author

@greptileai review

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.

1 participant