From 044a1e4fead05ab7d1be43625a50a5fdbf7dd7a4 Mon Sep 17 00:00:00 2001 From: Mahad Kalam <55807755+skyfallwastaken@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:29:04 +0000 Subject: [PATCH] Use Inertia deferred props + Prettier/svelte-check (#957) --- .github/workflows/ci.yml | 22 + .prettierrc.json | 13 + app/assets/tailwind/application.css | 2 - app/assets/tailwind/filterable_dashboard.css | 12 - .../api/v1/dashboard_stats_controller.rb | 23 - app/controllers/static_pages_controller.rb | 47 +- app/javascript/components/Button.svelte | 17 +- app/javascript/layouts/AppLayout.svelte | 114 +++-- app/javascript/pages/Docs/Index.svelte | 48 +- app/javascript/pages/Errors/NotFound.svelte | 6 +- app/javascript/pages/Home/SignedIn.svelte | 128 +++--- app/javascript/pages/Home/SignedOut.svelte | 4 +- .../pages/Home/signedIn/BanNotice.svelte | 50 ++- .../pages/Home/signedIn/Dashboard.svelte | 24 +- .../Home/signedIn/GitHubLinkBanner.svelte | 4 +- .../pages/Home/signedIn/IntervalSelect.svelte | 33 +- .../pages/Home/signedIn/MultiSelect.svelte | 29 +- .../Home/signedIn/ProjectTimeline.svelte | 5 +- .../Home/signedIn/ProjectTimelineChart.svelte | 35 +- .../pages/Home/signedIn/SetupNotice.svelte | 15 +- .../pages/Home/signedIn/StatCard.svelte | 3 +- .../pages/Users/Settings/Access.svelte | 48 +- .../pages/Users/Settings/Admin.svelte | 47 +- .../pages/Users/Settings/Badges.svelte | 34 +- .../pages/Users/Settings/Data.svelte | 71 ++- .../pages/Users/Settings/Integrations.svelte | 69 ++- .../pages/Users/Settings/Profile.svelte | 82 ++-- .../pages/Users/Settings/Shell.svelte | 16 +- .../pages/WakatimeSetup/Index.svelte | 11 +- .../pages/WakatimeSetup/Step2.svelte | 64 ++- .../pages/WakatimeSetup/Step3.svelte | 81 ++-- .../pages/WakatimeSetup/Stepper.svelte | 38 +- .../_dashboard_interval_selector.html.erb | 38 -- app/views/shared/_multi_select.html.erb | 23 - .../_filterable_dashboard.html.erb | 410 ------------------ .../_filterable_dashboard_content.html.erb | 138 ------ bun.lock | 8 + config/environments/test.rb | 4 + config/routes.rb | 4 - package-lock.json | 111 +++-- package.json | 9 +- tsconfig.svelte-check.json | 7 + 42 files changed, 816 insertions(+), 1131 deletions(-) create mode 100644 .prettierrc.json delete mode 100644 app/assets/tailwind/filterable_dashboard.css delete mode 100644 app/controllers/api/v1/dashboard_stats_controller.rb delete mode 100644 app/views/shared/_dashboard_interval_selector.html.erb delete mode 100644 app/views/shared/_multi_select.html.erb delete mode 100644 app/views/static_pages/_filterable_dashboard.html.erb delete mode 100644 app/views/static_pages/_filterable_dashboard_content.html.erb create mode 100644 tsconfig.svelte-check.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1db9e2f..f5bb9c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,28 @@ jobs: - name: Lint code for consistent style run: bin/rubocop -f github + frontend: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + + - name: Install JavaScript dependencies + run: npm ci + + - name: Run Svelte type checks + run: npm run check:svelte + + - name: Run Svelte formatting checks + run: npm run format:svelte:check + zeitwerk: runs-on: ubuntu-latest diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..a4e08f4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,13 @@ +{ + "plugins": ["prettier-plugin-svelte"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte", + "tabWidth": 2, + "useTabs": false + } + } + ] +} diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index f211a56..42610ce 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -3,7 +3,6 @@ @source "../../../node_modules/layerchart/dist/**/*.{svelte,js}"; @import "./main.css"; @import "./nav.css"; -@import "./filterable_dashboard.css"; body { @apply flex min-h-screen; @@ -291,4 +290,3 @@ html[data-theme="nord"] { background-color: var(--color-primary); opacity: 0.9; } - diff --git a/app/assets/tailwind/filterable_dashboard.css b/app/assets/tailwind/filterable_dashboard.css deleted file mode 100644 index 2030f56..0000000 --- a/app/assets/tailwind/filterable_dashboard.css +++ /dev/null @@ -1,12 +0,0 @@ -.container { - @apply max-w-6xl mx-auto my-0 px-4 py-8 pb-0; -} - -#filterable_dashboard_content.loading > div { - @apply grayscale opacity-70 pointer-events-none transition-[filter] duration-200 ease-in-out; -} - -.filter .option input[type="checkbox"]:checked::after { - content: ""; - @apply absolute left-1 top-1 w-1 h-2 border-white border-solid rotate-45 border-0 border-r-2 border-b-2; -} diff --git a/app/controllers/api/v1/dashboard_stats_controller.rb b/app/controllers/api/v1/dashboard_stats_controller.rb deleted file mode 100644 index a287a09..0000000 --- a/app/controllers/api/v1/dashboard_stats_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Api - module V1 - class DashboardStatsController < ApplicationController - include DashboardData - - before_action :require_session_user! - - def show - render json: { - filterable_dashboard_data: filterable_dashboard_data, - activity_graph: activity_graph_data, - today_stats: today_stats_data - } - end - - private - - def require_session_user! - render json: { error: "Unauthorized" }, status: :unauthorized unless current_user - end - end - end -end diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index 27da71b..4dea016 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -2,10 +2,6 @@ class StaticPagesController < InertiaController include DashboardData layout "inertia", only: :index - before_action :ensure_current_user, only: %i[ - filterable_dashboard - filterable_dashboard_content - ] def index if current_user @@ -108,32 +104,8 @@ class StaticPagesController < InertiaController render partial: "streak" end - def filterable_dashboard - load_dashboard_data - %i[project language operating_system editor category].each do |f| - instance_variable_set("@selected_#{f}", params[f]&.split(",") || []) - end - @selected_interval = params[:interval] - @selected_from = params[:from] - @selected_to = params[:to] - render partial: "filterable_dashboard" - end - - def filterable_dashboard_content - load_dashboard_data - render partial: "filterable_dashboard_content" - end - private - def load_dashboard_data - filterable_dashboard_data.each { |k, v| instance_variable_set("@#{k}", v) } - end - - def ensure_current_user - redirect_to(root_path, alert: "You must be logged in to view this page") unless current_user - end - def set_homepage_seo_content @page_title = @og_title = @twitter_title = "Hackatime - See How Much You Code" @meta_description = @og_description = @twitter_description = "Free and open source. Works with VS Code, JetBrains IDEs, vim, emacs, and 70+ other editors. Built and made free for teenagers by Hack Club." @@ -151,16 +123,15 @@ class StaticPagesController < InertiaController github_uid_blank: current_user&.github_uid.blank?, github_auth_path: github_auth_path, wakatime_setup_path: my_wakatime_setup_path, - dashboard_stats_url: api_v1_dashboard_stats_path( - interval: params[:interval], - from: params[:from], - to: params[:to], - project: params[:project], - language: params[:language], - editor: params[:editor], - operating_system: params[:operating_system], - category: params[:category] - ) + dashboard_stats: InertiaRails.defer { dashboard_stats_payload } + } + end + + def dashboard_stats_payload + { + filterable_dashboard_data: filterable_dashboard_data, + activity_graph: activity_graph_data, + today_stats: today_stats_data } end diff --git a/app/javascript/components/Button.svelte b/app/javascript/components/Button.svelte index ca8d966..88bb9df 100644 --- a/app/javascript/components/Button.svelte +++ b/app/javascript/components/Button.svelte @@ -1,13 +1,10 @@ {#if href} - - + + {@render children?.()} {:else} - {/if} diff --git a/app/javascript/layouts/AppLayout.svelte b/app/javascript/layouts/AppLayout.svelte index feb40dd..6ac54fe 100644 --- a/app/javascript/layouts/AppLayout.svelte +++ b/app/javascript/layouts/AppLayout.svelte @@ -2,7 +2,7 @@ import { Link, usePoll } from "@inertiajs/svelte"; import Button from "../components/Button.svelte"; import type { Snippet } from "svelte"; - import { onMount, onDestroy } from "svelte"; + import { onMount, onDestroy, untrack } from "svelte"; import plur from "plur"; type NavLink = { @@ -88,17 +88,20 @@ let navOpen = $state(false); let logoutOpen = $state(false); let currentlyExpanded = $state(false); - let flashVisible = $state(layout.nav.flash.length > 0); + let flashVisible = $state(false); let flashHiding = $state(false); const flashHideDelay = 6000; const flashExitDuration = 250; + const pollInterval = untrack( + () => layout.currently_hacking?.interval || 30000, + ); const toggleNav = () => (navOpen = !navOpen); const closeNav = () => (navOpen = false); const openLogout = () => (logoutOpen = true); const closeLogout = () => (logoutOpen = false); - usePoll(layout.currently_hacking?.interval || 30000, { + usePoll(pollInterval, { only: ["currently_hacking"], }); @@ -117,6 +120,13 @@ } }; + const handleModalBackdropKeydown = (e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + closeLogout(); + } + }; + const countLabel = () => `${layout.currently_hacking.count} ${plur("person", layout.currently_hacking.count)} currently hacking`; @@ -189,7 +199,8 @@ }; }; - const streakLabel = (streakDays: number) => (streakDays > 30 ? "30+" : `${streakDays}`); + const streakLabel = (streakDays: number) => + streakDays > 30 ? "30+" : `${streakDays}`; const adminLevelLabel = (adminLevel?: AdminLevel | null) => { if (adminLevel === "superadmin") return "Superadmin"; @@ -218,9 +229,7 @@ document.documentElement.setAttribute("data-theme", layout.theme.name); - const colorSchemeMeta = document.querySelector( - "meta[name='color-scheme']", - ); + const colorSchemeMeta = document.querySelector("meta[name='color-scheme']"); colorSchemeMeta?.setAttribute("content", layout.theme.color_scheme); const themeColorMeta = document.querySelector("meta[name='theme-color']"); @@ -312,7 +321,13 @@ /> - + Logout + {:else if link.inertia} + {link.label} {:else} - {#if link.inertia} - {link.label} - {:else} - {link.label} - {/if} + {link.label} {/if} {/each} @@ -584,8 +610,8 @@ > from {layout.footer.server_start_time_ago} ago. {plur("heartbeat", layout.footer.heartbeat_recent_count)} - ({layout.footer.heartbeat_recent_imported_count} imported) in the past - 24 hours. (DB: {layout.footer.query_count} + ({layout.footer.heartbeat_recent_imported_count} imported) in the past 24 + hours. (DB: {layout.footer.query_count} {plur("query", layout.footer.query_count)}, {layout.footer .query_cache_count} cached) (CACHE: {layout.footer.cache_hits} hits, {layout.footer.cache_misses} misses) ({layout.footer @@ -617,19 +643,21 @@
-
-
+
{countLabel()}
-
+ {#if currentlyExpanded} {#if layout.currently_hacking.users.length === 0} @@ -707,7 +735,11 @@ class:opacity-0={!logoutOpen} class:pointer-events-none={!logoutOpen} style="background-color: rgba(0, 0, 0, 0.5);backdrop-filter: blur(4px);" + role="button" + tabindex="0" + aria-label="Close logout dialog" onclick={(e) => e.target === e.currentTarget && closeLogout()} + onkeydown={handleModalBackdropKeydown} >
-

+

Woah hold on a sec

@@ -741,8 +775,7 @@ type="button" onclick={closeLogout} variant="dark" - class="w-full h-10 text-muted m-0" - >Go backGo back

@@ -753,10 +786,7 @@ value={layout.csrf_token} /> - diff --git a/app/javascript/pages/Docs/Index.svelte b/app/javascript/pages/Docs/Index.svelte index 7d484ea..98ea3b0 100644 --- a/app/javascript/pages/Docs/Index.svelte +++ b/app/javascript/pages/Docs/Index.svelte @@ -31,8 +31,16 @@ href="/my/wakatime_setup" class="flex flex-col items-center justify-center p-6 bg-surface border border-surface-200 rounded-lg hover:border-primary transition-colors" > - - + +

Quick Start

@@ -44,8 +52,16 @@ href="/docs/getting-started/installation" class="flex flex-col items-center justify-center p-6 bg-surface border border-surface-200 rounded-lg hover:border-primary transition-colors" > - - + +

Installation

Add to your editor

@@ -55,8 +71,16 @@ href="/api-docs" class="flex flex-col items-center justify-center p-6 bg-surface border border-surface-200 rounded-lg hover:border-primary transition-colors" > - - + +

API Docs

Interactive reference

@@ -66,8 +90,16 @@ href="/docs/oauth/oauth-apps" class="flex flex-col items-center justify-center p-6 bg-surface border border-surface-200 rounded-lg hover:border-primary transition-colors" > - - + +

OAuth Apps

Build integrations

diff --git a/app/javascript/pages/Errors/NotFound.svelte b/app/javascript/pages/Errors/NotFound.svelte index 048d5b8..2cd716f 100644 --- a/app/javascript/pages/Errors/NotFound.svelte +++ b/app/javascript/pages/Errors/NotFound.svelte @@ -22,11 +22,7 @@

{status_code}

{title}

{message}

-
diff --git a/app/javascript/pages/Home/SignedIn.svelte b/app/javascript/pages/Home/SignedIn.svelte index d3ebe50..cda0e05 100644 --- a/app/javascript/pages/Home/SignedIn.svelte +++ b/app/javascript/pages/Home/SignedIn.svelte @@ -1,5 +1,5 @@
@@ -149,33 +118,46 @@ {/if} -
- -
- {#if loading} - - {:else if todayStats} - - {/if} -
+ + {#snippet fallback()} +
+
+ +
+ + +
+ {/snippet} - - {#if loading} - - {:else if dashboardData} - - {/if} + {#snippet children({ reloading })} +
+ +
+ {#if dashboard_stats?.today_stats} + + {/if} +
- - {#if loading} - - {:else if activityGraph} - - {/if} -
+ + {#if dashboard_stats?.filterable_dashboard_data} + + {/if} + + + {#if dashboard_stats?.activity_graph} + + {/if} +
+ {/snippet} +
diff --git a/app/javascript/pages/Home/SignedOut.svelte b/app/javascript/pages/Home/SignedOut.svelte index 2c30d75..75fc63c 100644 --- a/app/javascript/pages/Home/SignedOut.svelte +++ b/app/javascript/pages/Home/SignedOut.svelte @@ -73,7 +73,9 @@
+
- - Hold up! Your account has been banned for suspicious activity. + + Hold up! Your account has been banned for suspicious activity.
-

What does this mean? Your account was convicted for fraud or abuse of Hackatime, such as using methods to gain an unfair advantage on the leaderboards or attempting to manipulate your coding time in any way. This restricts your access to participate in public leaderboards, but Hackatime will still track and display your time. This may also affect your ability to participate in current and future Hack Club events.

-

What can I do? Account bans are non-negotiable, and will not be removed unless determined to have been issued incorrectly. In that case, it will automatically be removed. We take fraud very seriously and have a zero-tolerance policy for abuse. If you believe this was a mistake, please DM the Fraud Department on Slack. We do not respond in any other channel, DM or thread.

-

Can I know what caused this? No. We do not disclose the patterns that were detected. Releasing this information would only benefit fraudsters. The fraud team regularly investigates claims of false bans to increase the effectiveness of our detection systems to combat fraud.

+

+ What does this mean? Your account was convicted for fraud or abuse of + Hackatime, such as using methods to gain an unfair advantage on the leaderboards + or attempting to manipulate your coding time in any way. This restricts your + access to participate in public leaderboards, but Hackatime will still track + and display your time. This may also affect your ability to participate in current + and future Hack Club events. +

+

+ What can I do? Account bans are non-negotiable, and will not be + removed unless determined to have been issued incorrectly. In that case, + it will automatically be removed. We take fraud very seriously and have a + zero-tolerance policy for abuse. If you believe this was a mistake, please + DM the + Fraud Department on Slack. We do not respond in any other channel, DM or thread. +

+

+ Can I know what caused this? No. We do not disclose the patterns that + were detected. Releasing this information would only benefit fraudsters. The + fraud team regularly investigates claims of false bans to increase the effectiveness + of our detection systems to combat fraud. +

diff --git a/app/javascript/pages/Home/signedIn/Dashboard.svelte b/app/javascript/pages/Home/signedIn/Dashboard.svelte index b9abe6d..3f2607b 100644 --- a/app/javascript/pages/Home/signedIn/Dashboard.svelte +++ b/app/javascript/pages/Home/signedIn/Dashboard.svelte @@ -12,11 +12,9 @@ onFiltersChange, }: { data: Record; - onFiltersChange?: (search: string) => Promise | void; + onFiltersChange?: (search: string) => void; } = $props(); - let loading = $state(false); - const langStats = $derived( (data.language_stats || {}) as Record, ); @@ -36,7 +34,7 @@ const capitalize = (s: string) => s ? s.charAt(0).toUpperCase() + s.slice(1) : ""; - async function applyFilters(overrides: Record) { + function applyFilters(overrides: Record) { const current = new URL(window.location.href); for (const [k, v] of Object.entries(overrides)) { if (v) { @@ -45,31 +43,23 @@ current.searchParams.delete(k); } } - - window.history.pushState({}, "", current.pathname + current.search); - - loading = true; - try { - await onFiltersChange?.(current.search); - } finally { - loading = false; - } + onFiltersChange?.(current.search); } function onIntervalChange(interval: string, from: string, to: string) { if (from || to) { - void applyFilters({ interval: "custom", from, to }); + applyFilters({ interval: "custom", from, to }); } else { - void applyFilters({ interval, from: "", to: "" }); + applyFilters({ interval, from: "", to: "" }); } } function onFilterChange(param: string, selected: string[]) { - void applyFilters({ [param]: selected.join(",") }); + applyFilters({ [param]: selected.join(",") }); } -
+
-
+
{ if (open) { document.addEventListener("click", handleClickOutside, true); - return () => document.removeEventListener("click", handleClickOutside, true); + return () => + document.removeEventListener("click", handleClickOutside, true); } }); @@ -82,11 +83,15 @@
- + Date Range -
+
@@ -112,10 +127,14 @@
{#if open} -
+
{#each INTERVALS as interval} -