mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 16:38:23 +00:00
Use Inertia deferred props + Prettier/svelte-check (#957)
This commit is contained in:
parent
d203e1118f
commit
044a1e4fea
42 changed files with 816 additions and 1131 deletions
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
13
.prettierrc.json
Normal file
13
.prettierrc.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { Link } from "@inertiajs/svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
type ButtonType = "button" | "submit" | "reset";
|
||||
type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl";
|
||||
type ButtonVariant =
|
||||
| "primary"
|
||||
| "surface"
|
||||
| "dark"
|
||||
| "outlinePrimary";
|
||||
type ButtonVariant = "primary" | "surface" | "dark" | "outlinePrimary";
|
||||
|
||||
let {
|
||||
href = "",
|
||||
|
|
@ -15,6 +12,7 @@
|
|||
size = "md",
|
||||
variant = "primary",
|
||||
unstyled = false,
|
||||
children,
|
||||
class: className = "",
|
||||
...rest
|
||||
}: {
|
||||
|
|
@ -23,6 +21,7 @@
|
|||
size?: ButtonSize;
|
||||
variant?: ButtonVariant;
|
||||
unstyled?: boolean;
|
||||
children?: Snippet;
|
||||
class?: string;
|
||||
[key: string]: unknown;
|
||||
} = $props();
|
||||
|
|
@ -59,11 +58,11 @@
|
|||
</script>
|
||||
|
||||
{#if href}
|
||||
<Link href={href} class={classes} {...rest}>
|
||||
<slot />
|
||||
<Link {href} class={classes} {...rest}>
|
||||
{@render children?.()}
|
||||
</Link>
|
||||
{:else}
|
||||
<button type={type} class={classes} {...rest}>
|
||||
<slot />
|
||||
<button {type} class={classes} {...rest}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
<div class="nav-overlay" class:open={navOpen} onclick={closeNav}></div>
|
||||
<Button
|
||||
type="button"
|
||||
unstyled
|
||||
class={`nav-overlay ${navOpen ? "open" : ""}`}
|
||||
aria-label="Close navigation"
|
||||
onclick={closeNav}
|
||||
/>
|
||||
|
||||
<aside
|
||||
class="flex flex-col min-h-screen w-52 bg-dark text-surface-content px-3 py-4 rounded-r-lg overflow-y-auto lg:block"
|
||||
|
|
@ -326,7 +341,10 @@
|
|||
class="flex flex-col items-center gap-2 pb-3 border-b border-darkless"
|
||||
>
|
||||
{#if layout.nav.current_user}
|
||||
<div class="user-info flex items-center gap-2" title={layout.nav.current_user.title}>
|
||||
<div
|
||||
class="user-info flex items-center gap-2"
|
||||
title={layout.nav.current_user.title}
|
||||
>
|
||||
{#if layout.nav.current_user.avatar_url}
|
||||
<img
|
||||
src={layout.nav.current_user.avatar_url}
|
||||
|
|
@ -343,7 +361,8 @@
|
|||
{#if layout.nav.current_user.country_code}
|
||||
<span
|
||||
class="flex items-center"
|
||||
title={layout.nav.current_user.country_name || layout.nav.current_user.country_code}
|
||||
title={layout.nav.current_user.country_name ||
|
||||
layout.nav.current_user.country_code}
|
||||
>
|
||||
{countryFlagEmoji(layout.nav.current_user.country_code)}
|
||||
</span>
|
||||
|
|
@ -351,10 +370,14 @@
|
|||
</div>
|
||||
|
||||
{#if layout.nav.current_user.streak_days && layout.nav.current_user.streak_days > 0}
|
||||
{@const streakTheme = streakThemeClasses(layout.nav.current_user.streak_days)}
|
||||
{@const streakTheme = streakThemeClasses(
|
||||
layout.nav.current_user.streak_days,
|
||||
)}
|
||||
<div
|
||||
class={`inline-flex items-center gap-1 px-2 py-1 bg-gradient-to-r ${streakTheme.bg} border ${streakTheme.bc} rounded-lg transition-all duration-200 ${streakTheme.hbg} group`}
|
||||
title={layout.nav.current_user.streak_days > 30 ? "30+ daily streak" : `${layout.nav.current_user.streak_days} day streak`}
|
||||
title={layout.nav.current_user.streak_days > 30
|
||||
? "30+ daily streak"
|
||||
: `${layout.nav.current_user.streak_days} day streak`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -369,9 +392,13 @@
|
|||
></path>
|
||||
</svg>
|
||||
|
||||
<span class={`text-md font-semibold ${streakTheme.tc} transition-colors duration-200`}>
|
||||
<span
|
||||
class={`text-md font-semibold ${streakTheme.tc} transition-colors duration-200`}
|
||||
>
|
||||
{streakLabel(layout.nav.current_user.streak_days)}
|
||||
<span class={`ml-1 font-normal ${streakTheme.tm}`}>day streak</span>
|
||||
<span class={`ml-1 font-normal ${streakTheme.tm}`}
|
||||
>day streak</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -401,22 +428,21 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={openLogout}
|
||||
class={`${navLinkClass(false)} cursor-pointer w-full text-left`}>Logout</button
|
||||
class={`${navLinkClass(false)} cursor-pointer w-full text-left`}
|
||||
>Logout</button
|
||||
>
|
||||
{:else if link.inertia}
|
||||
<Link
|
||||
href={link.href || "#"}
|
||||
onclick={handleNavLinkClick}
|
||||
class={navLinkClass(link.active)}>{link.label}</Link
|
||||
>
|
||||
{:else}
|
||||
{#if link.inertia}
|
||||
<Link
|
||||
href={link.href || "#"}
|
||||
onclick={handleNavLinkClick}
|
||||
class={navLinkClass(link.active)}>{link.label}</Link
|
||||
>
|
||||
{:else}
|
||||
<a
|
||||
href={link.href || "#"}
|
||||
onclick={handleNavLinkClick}
|
||||
class={navLinkClass(link.active)}>{link.label}</a
|
||||
>
|
||||
{/if}
|
||||
<a
|
||||
href={link.href || "#"}
|
||||
onclick={handleNavLinkClick}
|
||||
class={navLinkClass(link.active)}>{link.label}</a
|
||||
>
|
||||
{/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 @@
|
|||
<div
|
||||
class="fixed top-0 right-5 max-w-sm max-h-[80vh] bg-dark border border-darkless rounded-b-xl shadow-lg z-1000 overflow-hidden transform transition-transform duration-300 ease-out"
|
||||
>
|
||||
<div
|
||||
<Button
|
||||
type="button"
|
||||
unstyled
|
||||
class="currently-hacking p-3 bg-dark cursor-pointer select-none flex items-center justify-between"
|
||||
aria-expanded={currentlyExpanded}
|
||||
aria-label="Toggle currently hacking users"
|
||||
onclick={toggleCurrentlyHacking}
|
||||
>
|
||||
<div class="text-surface-content text-sm font-medium">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-green animate-pulse mr-2"
|
||||
></div>
|
||||
<div class="w-2 h-2 rounded-full bg-green animate-pulse mr-2"></div>
|
||||
<span class="text-base">{countLabel()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
{#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}
|
||||
>
|
||||
<div
|
||||
class={`bg-dark border border-primary rounded-lg p-6 max-w-md w-full mx-4 flex flex-col items-center justify-center transform transition-transform duration-300 ease-in-out ${logoutOpen ? "scale-100" : "scale-95"}`}
|
||||
|
|
@ -727,7 +759,9 @@
|
|||
</svg>
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl font-bold text-surface-content mb-2 text-center w-full">
|
||||
<h3
|
||||
class="text-2xl font-bold text-surface-content mb-2 text-center w-full"
|
||||
>
|
||||
Woah hold on a sec
|
||||
</h3>
|
||||
<p class="text-muted mb-6 text-center w-full">
|
||||
|
|
@ -741,8 +775,7 @@
|
|||
type="button"
|
||||
onclick={closeLogout}
|
||||
variant="dark"
|
||||
class="w-full h-10 text-muted m-0"
|
||||
>Go back</Button
|
||||
class="w-full h-10 text-muted m-0">Go back</Button
|
||||
>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
|
|
@ -753,10 +786,7 @@
|
|||
value={layout.csrf_token}
|
||||
/>
|
||||
<input type="hidden" name="_method" value="delete" />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
class="w-full h-10 m-0"
|
||||
<Button type="submit" variant="primary" class="w-full h-10 m-0"
|
||||
>Log out now</Button
|
||||
>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<svg class="w-6 h-6 mb-2 text-primary" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z" clip-rule="evenodd" />
|
||||
<svg
|
||||
class="w-6 h-6 mb-2 text-primary"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M14.615 1.595a.75.75 0 01.359.852L12.982 9.75h7.268a.75.75 0 01.548 1.262l-10.5 11.25a.75.75 0 01-1.272-.71l1.992-7.302H3.75a.75.75 0 01-.548-1.262l10.5-11.25a.75.75 0 01.913-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="font-semibold text-surface-content">Quick Start</h3>
|
||||
<p class="text-sm text-muted text-center mt-1">
|
||||
|
|
@ -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"
|
||||
>
|
||||
<svg class="w-6 h-6 mb-2 text-primary" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M2.25 5.25a3 3 0 013-3h13.5a3 3 0 013 3V15a3 3 0 01-3 3h-3v.257c0 .597.237 1.17.659 1.591l.621.622H7.071l.621-.622A2.25 2.25 0 008.25 18.257V18h-3a3 3 0 01-3-3V5.25zm1.5 0v7.5a1.5 1.5 0 001.5 1.5h13.5a1.5 1.5 0 001.5-1.5v-7.5a1.5 1.5 0 00-1.5-1.5H5.25a1.5 1.5 0 00-1.5 1.5z" clip-rule="evenodd" />
|
||||
<svg
|
||||
class="w-6 h-6 mb-2 text-primary"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2.25 5.25a3 3 0 013-3h13.5a3 3 0 013 3V15a3 3 0 01-3 3h-3v.257c0 .597.237 1.17.659 1.591l.621.622H7.071l.621-.622A2.25 2.25 0 008.25 18.257V18h-3a3 3 0 01-3-3V5.25zm1.5 0v7.5a1.5 1.5 0 001.5 1.5h13.5a1.5 1.5 0 001.5-1.5v-7.5a1.5 1.5 0 00-1.5-1.5H5.25a1.5 1.5 0 00-1.5 1.5z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="font-semibold text-surface-content">Installation</h3>
|
||||
<p class="text-sm text-muted text-center mt-1">Add to your editor</p>
|
||||
|
|
@ -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"
|
||||
>
|
||||
<svg class="w-6 h-6 mb-2 text-primary" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M14.447 3.027a.75.75 0 01.527.92l-4.5 16.5a.75.75 0 01-1.448-.394l4.5-16.5a.75.75 0 01.921-.526zM16.72 6.22a.75.75 0 011.06 0l5.25 5.25a.75.75 0 010 1.06l-5.25 5.25a.75.75 0 11-1.06-1.06L21.44 12l-4.72-4.72a.75.75 0 010-1.06zm-9.44 0a.75.75 0 010 1.06L2.56 12l4.72 4.72a.75.75 0 11-1.06 1.06L.97 12.53a.75.75 0 010-1.06l5.25-5.25a.75.75 0 011.06 0z" clip-rule="evenodd" />
|
||||
<svg
|
||||
class="w-6 h-6 mb-2 text-primary"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M14.447 3.027a.75.75 0 01.527.92l-4.5 16.5a.75.75 0 01-1.448-.394l4.5-16.5a.75.75 0 01.921-.526zM16.72 6.22a.75.75 0 011.06 0l5.25 5.25a.75.75 0 010 1.06l-5.25 5.25a.75.75 0 11-1.06-1.06L21.44 12l-4.72-4.72a.75.75 0 010-1.06zm-9.44 0a.75.75 0 010 1.06L2.56 12l4.72 4.72a.75.75 0 11-1.06 1.06L.97 12.53a.75.75 0 010-1.06l5.25-5.25a.75.75 0 011.06 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="font-semibold text-surface-content">API Docs</h3>
|
||||
<p class="text-sm text-muted text-center mt-1">Interactive reference</p>
|
||||
|
|
@ -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"
|
||||
>
|
||||
<svg class="w-6 h-6 mb-2 text-primary" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M15.75 1.5a6.75 6.75 0 00-6.651 7.906c.067.39-.032.717-.221.906l-6.5 6.499a3 3 0 00-.878 2.121v2.818c0 .414.336.75.75.75H6a.75.75 0 00.75-.75v-1.5h1.5A.75.75 0 009 19.5V18h1.5a.75.75 0 00.53-.22l2.658-2.658c.19-.189.517-.288.906-.22A6.75 6.75 0 1015.75 1.5zm0 3a.75.75 0 000 1.5A2.25 2.25 0 0118 8.25a.75.75 0 001.5 0 3.75 3.75 0 00-3.75-3.75z" clip-rule="evenodd" />
|
||||
<svg
|
||||
class="w-6 h-6 mb-2 text-primary"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.75 1.5a6.75 6.75 0 00-6.651 7.906c.067.39-.032.717-.221.906l-6.5 6.499a3 3 0 00-.878 2.121v2.818c0 .414.336.75.75.75H6a.75.75 0 00.75-.75v-1.5h1.5A.75.75 0 009 19.5V18h1.5a.75.75 0 00.53-.22l2.658-2.658c.19-.189.517-.288.906-.22A6.75 6.75 0 1015.75 1.5zm0 3a.75.75 0 000 1.5A2.25 2.25 0 0118 8.25a.75.75 0 001.5 0 3.75 3.75 0 00-3.75-3.75z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<h3 class="font-semibold text-surface-content">OAuth Apps</h3>
|
||||
<p class="text-sm text-muted text-center mt-1">Build integrations</p>
|
||||
|
|
|
|||
|
|
@ -22,11 +22,7 @@
|
|||
<h1 class="text-6xl font-bold text-primary mb-4">{status_code}</h1>
|
||||
<h2 class="text-2xl font-semibold text-surface-content mb-4">{title}</h2>
|
||||
<p class="text-secondary mb-8">{message}</p>
|
||||
<Button
|
||||
href="/"
|
||||
size="lg"
|
||||
class="hover:brightness-110 transition-all"
|
||||
>
|
||||
<Button href="/" size="lg" class="hover:brightness-110 transition-all">
|
||||
Go Home
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { Deferred, router } from "@inertiajs/svelte";
|
||||
import type { ActivityGraphData } from "../../types/index";
|
||||
import BanNotice from "./signedIn/BanNotice.svelte";
|
||||
import GitHubLinkBanner from "./signedIn/GitHubLinkBanner.svelte";
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
github_uid_blank,
|
||||
github_auth_path,
|
||||
wakatime_setup_path,
|
||||
dashboard_stats_url,
|
||||
dashboard_stats,
|
||||
}: {
|
||||
flavor_text: string;
|
||||
trust_level_red: boolean;
|
||||
|
|
@ -69,53 +69,22 @@
|
|||
github_uid_blank: boolean;
|
||||
github_auth_path: string;
|
||||
wakatime_setup_path: string;
|
||||
dashboard_stats_url: string;
|
||||
dashboard_stats?: {
|
||||
filterable_dashboard_data: FilterableDashboardData;
|
||||
activity_graph: ActivityGraphData;
|
||||
today_stats: TodayStats;
|
||||
};
|
||||
} = $props();
|
||||
|
||||
let loading = $state(true);
|
||||
let todayStats = $state<TodayStats | null>(null);
|
||||
let dashboardData = $state<FilterableDashboardData | null>(null);
|
||||
let activityGraph = $state<ActivityGraphData | null>(null);
|
||||
let requestSequence = 0;
|
||||
|
||||
function buildDashboardStatsUrl(search: string) {
|
||||
const url = new URL(dashboard_stats_url, window.location.origin);
|
||||
if (search.startsWith("?")) {
|
||||
url.search = search;
|
||||
} else if (search) {
|
||||
url.search = `?${search}`;
|
||||
} else {
|
||||
url.search = "";
|
||||
}
|
||||
return `${url.pathname}${url.search}`;
|
||||
function refreshDashboardData(search: string) {
|
||||
router.visit(`${window.location.pathname}${search}`, {
|
||||
only: ["dashboard_stats"],
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
replace: true,
|
||||
async: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshDashboardData(search: string) {
|
||||
const requestId = ++requestSequence;
|
||||
try {
|
||||
const res = await fetch(buildDashboardStatsUrl(search), {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) return;
|
||||
|
||||
const data = await res.json();
|
||||
if (requestId !== requestSequence) return;
|
||||
|
||||
todayStats = data.today_stats;
|
||||
dashboardData = data.filterable_dashboard_data;
|
||||
activityGraph = data.activity_graph;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await refreshDashboardData(window.location.search);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
|
@ -149,33 +118,46 @@
|
|||
<GitHubLinkBanner {github_auth_path} />
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
<!-- Today Stats -->
|
||||
<div>
|
||||
{#if loading}
|
||||
<TodaySentenceSkeleton />
|
||||
{:else if todayStats}
|
||||
<TodaySentence
|
||||
show_logged_time_sentence={todayStats.show_logged_time_sentence}
|
||||
todays_duration_display={todayStats.todays_duration_display}
|
||||
todays_languages={todayStats.todays_languages}
|
||||
todays_editors={todayStats.todays_editors}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<Deferred data="dashboard_stats">
|
||||
{#snippet fallback()}
|
||||
<div class="flex flex-col gap-8">
|
||||
<div>
|
||||
<TodaySentenceSkeleton />
|
||||
</div>
|
||||
<DashboardSkeleton />
|
||||
<ActivityGraphSkeleton />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- Main Dashboard -->
|
||||
{#if loading}
|
||||
<DashboardSkeleton />
|
||||
{:else if dashboardData}
|
||||
<Dashboard data={dashboardData} onFiltersChange={refreshDashboardData} />
|
||||
{/if}
|
||||
{#snippet children({ reloading })}
|
||||
<div class="flex flex-col gap-8" class:opacity-60={reloading}>
|
||||
<!-- Today Stats -->
|
||||
<div>
|
||||
{#if dashboard_stats?.today_stats}
|
||||
<TodaySentence
|
||||
show_logged_time_sentence={dashboard_stats.today_stats
|
||||
.show_logged_time_sentence}
|
||||
todays_duration_display={dashboard_stats.today_stats
|
||||
.todays_duration_display}
|
||||
todays_languages={dashboard_stats.today_stats.todays_languages}
|
||||
todays_editors={dashboard_stats.today_stats.todays_editors}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Activity Graph -->
|
||||
{#if loading}
|
||||
<ActivityGraphSkeleton />
|
||||
{:else if activityGraph}
|
||||
<ActivityGraph data={activityGraph} />
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Main Dashboard -->
|
||||
{#if dashboard_stats?.filterable_dashboard_data}
|
||||
<Dashboard
|
||||
data={dashboard_stats.filterable_dashboard_data}
|
||||
onFiltersChange={refreshDashboardData}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Activity Graph -->
|
||||
{#if dashboard_stats?.activity_graph}
|
||||
<ActivityGraph data={dashboard_stats.activity_graph} />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Deferred>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,9 @@
|
|||
<div class="hidden md:flex gap-8 text-sm font-medium text-text-muted">
|
||||
<a href="#stats" class="hover:text-white transition-colors">Stats</a>
|
||||
<a href="#editors" class="hover:text-white transition-colors">Editors</a>
|
||||
<Link href="/docs" class="hover:text-white transition-colors">Developers</Link>
|
||||
<Link href="/docs" class="hover:text-white transition-colors"
|
||||
>Developers</Link
|
||||
>
|
||||
</div>
|
||||
<div class="min-w-[140px] flex justify-end">
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -1,11 +1,49 @@
|
|||
<div class="text-primary bg-red-500/10 border-2 border-red-500/20 p-4 text-center rounded-lg mb-4">
|
||||
<div
|
||||
class="text-primary bg-red-500/10 border-2 border-red-500/20 p-4 text-center rounded-lg mb-4"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M8 14.5a6.5 6.5 0 1 0 0-13a6.5 6.5 0 0 0 0 13M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m1-5a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-.25-6.25a.75.75 0 0 0-1.5 0v3.5a.75.75 0 0 0 1.5 0z" clip-rule="evenodd" /></svg>
|
||||
<span class="text-3xl font-bold block ml-2">Hold up! Your account has been banned for suspicious activity.</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 16 16"
|
||||
><path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M8 14.5a6.5 6.5 0 1 0 0-13a6.5 6.5 0 0 0 0 13M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m1-5a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-.25-6.25a.75.75 0 0 0-1.5 0v3.5a.75.75 0 0 0 1.5 0z"
|
||||
clip-rule="evenodd"
|
||||
/></svg
|
||||
>
|
||||
<span class="text-3xl font-bold block ml-2"
|
||||
>Hold up! Your account has been banned for suspicious activity.</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-primary text-left text-lg mb-2"><b>What does this mean?</b> 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.</p>
|
||||
<p class="text-primary text-left text-lg mb-2"><b>What can I do?</b> 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 <a href="https://hackclub.slack.com/team/U091HC53CE8" target="_blank" class="underline">Fraud Department</a> on Slack. We do not respond in any other channel, DM or thread.</p>
|
||||
<p class="text-primary text-left text-lg mb-0"><b>Can I know what caused this?</b> 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.</p>
|
||||
<p class="text-primary text-left text-lg mb-2">
|
||||
<b>What does this mean?</b> 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.
|
||||
</p>
|
||||
<p class="text-primary text-left text-lg mb-2">
|
||||
<b>What can I do?</b> 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
|
||||
<a
|
||||
href="https://hackclub.slack.com/team/U091HC53CE8"
|
||||
target="_blank"
|
||||
class="underline">Fraud Department</a
|
||||
> on Slack. We do not respond in any other channel, DM or thread.
|
||||
</p>
|
||||
<p class="text-primary text-left text-lg mb-0">
|
||||
<b>Can I know what caused this?</b> 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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,11 +12,9 @@
|
|||
onFiltersChange,
|
||||
}: {
|
||||
data: Record<string, any>;
|
||||
onFiltersChange?: (search: string) => Promise<void> | void;
|
||||
onFiltersChange?: (search: string) => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
|
||||
const langStats = $derived(
|
||||
(data.language_stats || {}) as Record<string, number>,
|
||||
);
|
||||
|
|
@ -36,7 +34,7 @@
|
|||
const capitalize = (s: string) =>
|
||||
s ? s.charAt(0).toUpperCase() + s.slice(1) : "";
|
||||
|
||||
async function applyFilters(overrides: Record<string, string>) {
|
||||
function applyFilters(overrides: Record<string, string>) {
|
||||
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(",") });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 w-full" class:opacity-60={loading}>
|
||||
<div class="flex flex-col gap-6 w-full">
|
||||
<!-- Filters -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 mb-2">
|
||||
<IntervalSelect
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
</script>
|
||||
|
||||
<div class="bg-dark border border-primary/40 rounded-xl p-4 md:p-5 mb-4">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div
|
||||
class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@
|
|||
$effect(() => {
|
||||
if (open) {
|
||||
document.addEventListener("click", handleClickOutside, true);
|
||||
return () => document.removeEventListener("click", handleClickOutside, true);
|
||||
return () =>
|
||||
document.removeEventListener("click", handleClickOutside, true);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -82,11 +83,15 @@
|
|||
</script>
|
||||
|
||||
<div class="filter relative" bind:this={container}>
|
||||
<span class="block text-xs font-medium mb-1.5 text-secondary/80 uppercase tracking-wider">
|
||||
<span
|
||||
class="block text-xs font-medium mb-1.5 text-secondary/80 uppercase tracking-wider"
|
||||
>
|
||||
Date Range
|
||||
</span>
|
||||
|
||||
<div class="group flex items-center border border-surface-200 rounded-lg bg-surface-100 m-0 p-0 transition-all duration-200 hover:border-surface-300 hover:bg-surface-200">
|
||||
<div
|
||||
class="group flex items-center border border-surface-200 rounded-lg bg-surface-100 m-0 p-0 transition-all duration-200 hover:border-surface-300 hover:bg-surface-200"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
unstyled
|
||||
|
|
@ -94,8 +99,18 @@
|
|||
onclick={() => (open = !open)}
|
||||
>
|
||||
<span>{displayLabel}</span>
|
||||
<svg class="w-4 h-4 text-secondary/60 transition-transform duration-200 group-hover:text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
<svg
|
||||
class="w-4 h-4 text-secondary/60 transition-transform duration-200 group-hover:text-secondary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
></path>
|
||||
</svg>
|
||||
</Button>
|
||||
|
||||
|
|
@ -112,10 +127,14 @@
|
|||
</div>
|
||||
|
||||
{#if open}
|
||||
<div class="absolute top-full left-0 right-0 min-w-64 bg-darkless border border-surface-200 rounded-lg mt-2 shadow-xl shadow-black/50 z-1000 p-2">
|
||||
<div
|
||||
class="absolute top-full left-0 right-0 min-w-64 bg-darkless border border-surface-200 rounded-lg mt-2 shadow-xl shadow-black/50 z-1000 p-2"
|
||||
>
|
||||
<div class="overflow-y-auto m-0 max-h-56">
|
||||
{#each INTERVALS as interval}
|
||||
<label class="flex items-center px-3 py-2.5 cursor-pointer text-sm text-muted m-0 bg-transparent rounded-md hover:bg-dark transition-colors duration-150">
|
||||
<label
|
||||
class="flex items-center px-3 py-2.5 cursor-pointer text-sm text-muted m-0 bg-transparent rounded-md hover:bg-dark transition-colors duration-150"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="interval"
|
||||
|
|
|
|||
|
|
@ -68,18 +68,26 @@
|
|||
</script>
|
||||
|
||||
<div class="filter relative" bind:this={container}>
|
||||
<span class="block text-xs font-medium mb-1.5 text-secondary/80 uppercase tracking-wider">
|
||||
<span
|
||||
class="block text-xs font-medium mb-1.5 text-secondary/80 uppercase tracking-wider"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
|
||||
<div class="group flex items-center border border-surface-200 rounded-lg bg-surface-100 m-0 p-0 transition-all duration-200 hover:border-surface-300 hover:bg-surface-200">
|
||||
<div
|
||||
class="group flex items-center border border-surface-200 rounded-lg bg-surface-100 m-0 p-0 transition-all duration-200 hover:border-surface-300 hover:bg-surface-200"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
unstyled
|
||||
class="flex-1 px-3 py-2.5 text-sm cursor-pointer select-none text-surface-content m-0 bg-transparent flex items-center justify-between border-0 min-w-0"
|
||||
onclick={() => (open = !open)}
|
||||
>
|
||||
<span class="truncate {selected.length === 0 ? 'text-surface-content/60' : ''}">
|
||||
<span
|
||||
class="truncate {selected.length === 0
|
||||
? 'text-surface-content/60'
|
||||
: ''}"
|
||||
>
|
||||
{displayText}
|
||||
</span>
|
||||
<svg
|
||||
|
|
@ -88,7 +96,12 @@
|
|||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
|
||||
|
|
@ -105,7 +118,9 @@
|
|||
</div>
|
||||
|
||||
{#if open}
|
||||
<div class="absolute top-full left-0 right-0 min-w-64 bg-darkless border border-surface-200 rounded-lg mt-2 shadow-xl shadow-black/50 z-1000 p-2">
|
||||
<div
|
||||
class="absolute top-full left-0 right-0 min-w-64 bg-darkless border border-surface-200 rounded-lg mt-2 shadow-xl shadow-black/50 z-1000 p-2"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
|
|
@ -115,7 +130,9 @@
|
|||
|
||||
<div class="overflow-y-auto m-0 max-h-64">
|
||||
{#each filtered as value}
|
||||
<label class="flex items-center px-3 py-2.5 cursor-pointer text-sm text-muted m-0 bg-transparent rounded-md hover:bg-dark transition-colors duration-150">
|
||||
<label
|
||||
class="flex items-center px-3 py-2.5 cursor-pointer text-sm text-muted m-0 bg-transparent rounded-md hover:bg-dark transition-colors duration-150"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.includes(value)}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@
|
|||
{#if entries.length > 0}
|
||||
<div class="flex flex-col gap-2 max-h-96 overflow-y-auto">
|
||||
{#each entries as [week, stats]}
|
||||
{@const total = Object.values(stats).reduce(
|
||||
(a, v) => a + (v || 0),
|
||||
0,
|
||||
)}
|
||||
{@const total = Object.values(stats).reduce((a, v) => a + (v || 0), 0)}
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-28 text-sm text-muted">{week}</div>
|
||||
<div class="flex-1 bg-darkless rounded h-3 overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -8,10 +8,26 @@
|
|||
} = $props();
|
||||
|
||||
const PIE_COLORS = [
|
||||
"#60a5fa", "#f472b6", "#fb923c", "#facc15", "#4ade80",
|
||||
"#2dd4bf", "#a78bfa", "#f87171", "#38bdf8", "#e879f9",
|
||||
"#34d399", "#fbbf24", "#818cf8", "#fb7185", "#22d3ee",
|
||||
"#a3e635", "#c084fc", "#f97316", "#14b8a6", "#8b5cf6",
|
||||
"#60a5fa",
|
||||
"#f472b6",
|
||||
"#fb923c",
|
||||
"#facc15",
|
||||
"#4ade80",
|
||||
"#2dd4bf",
|
||||
"#a78bfa",
|
||||
"#f87171",
|
||||
"#38bdf8",
|
||||
"#e879f9",
|
||||
"#34d399",
|
||||
"#fbbf24",
|
||||
"#818cf8",
|
||||
"#fb7185",
|
||||
"#22d3ee",
|
||||
"#a3e635",
|
||||
"#c084fc",
|
||||
"#f97316",
|
||||
"#14b8a6",
|
||||
"#8b5cf6",
|
||||
];
|
||||
|
||||
const sortedWeeks = $derived(Object.keys(weeklyStats).sort());
|
||||
|
|
@ -74,7 +90,10 @@
|
|||
|
||||
type TimelineDatum = Record<string, string | number>;
|
||||
|
||||
function getSeriesValue(datum: TimelineDatum | null | undefined, key: string): number {
|
||||
function getSeriesValue(
|
||||
datum: TimelineDatum | null | undefined,
|
||||
key: string,
|
||||
): number {
|
||||
const value = datum?.[key];
|
||||
return typeof value === "number" ? value : 0;
|
||||
}
|
||||
|
|
@ -83,7 +102,9 @@
|
|||
<div
|
||||
class="bg-dark/50 border border-surface-200 rounded-xl p-6 flex flex-col min-h-[400px]"
|
||||
>
|
||||
<h2 class="mb-4 text-lg font-semibold text-surface-content/90">Project Timeline</h2>
|
||||
<h2 class="mb-4 text-lg font-semibold text-surface-content/90">
|
||||
Project Timeline
|
||||
</h2>
|
||||
{#if data.length > 0}
|
||||
<div class="h-[350px]">
|
||||
<BarChart
|
||||
|
|
@ -108,7 +129,7 @@
|
|||
{@const value = getSeriesValue(data, s.key)}
|
||||
<Tooltip.Item
|
||||
label={s.label ?? s.key}
|
||||
value={value}
|
||||
{value}
|
||||
color={s.color}
|
||||
format={formatDuration}
|
||||
valueAlign="right"
|
||||
|
|
|
|||
|
|
@ -19,13 +19,18 @@
|
|||
|
||||
<div class="text-left mt-2 mb-4 flex flex-col">
|
||||
<p class="mb-4 text-xl text-primary">
|
||||
Hello friend! Looks like you are new around here, let's get you set up
|
||||
so you can start tracking your coding time.
|
||||
Hello friend! Looks like you are new around here, let's get you set up so
|
||||
you can start tracking your coding time.
|
||||
</p>
|
||||
<div class="mb-4 rounded-xl border border-primary/40 bg-surface-100/70 p-4 md:p-5">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div
|
||||
class="mb-4 rounded-xl border border-primary/40 bg-surface-100/70 p-4 md:p-5"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"
|
||||
>
|
||||
<p class="m-0 text-base font-medium text-surface-content">
|
||||
Finish setup once and we'll start tracking your coding time automatically.
|
||||
Finish setup once and we'll start tracking your coding time
|
||||
automatically.
|
||||
</p>
|
||||
<Button
|
||||
href={wakatime_setup_path}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@
|
|||
{value || "—"}
|
||||
</div>
|
||||
{#if subtitle}
|
||||
<span class="absolute bottom-2 left-4 text-xs text-secondary font-normal opacity-70"
|
||||
<span
|
||||
class="absolute bottom-2 left-4 text-xs text-secondary font-normal opacity-70"
|
||||
>{subtitle}</span
|
||||
>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -89,20 +89,21 @@
|
|||
>
|
||||
<div class="space-y-8">
|
||||
<section>
|
||||
<h2 class="text-xl font-semibold text-surface-content">Time Tracking Setup</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
Time Tracking Setup
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Use the setup guide if you are configuring a new editor or device.
|
||||
</p>
|
||||
<Button
|
||||
href={paths.wakatime_setup_path}
|
||||
class="mt-4"
|
||||
>
|
||||
<Button href={paths.wakatime_setup_path} class="mt-4">
|
||||
Open setup guide
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
<section id="user_hackatime_extension">
|
||||
<h2 class="text-xl font-semibold text-surface-content">Extension Display</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
Extension Display
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Choose how coding time appears in the extension status text.
|
||||
</p>
|
||||
|
|
@ -111,7 +112,10 @@
|
|||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
|
||||
<div>
|
||||
<label for="extension_type" class="mb-2 block text-sm text-surface-content">
|
||||
<label
|
||||
for="extension_type"
|
||||
class="mb-2 block text-sm text-surface-content"
|
||||
>
|
||||
Display style
|
||||
</label>
|
||||
<select
|
||||
|
|
@ -126,12 +130,7 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
Save extension settings
|
||||
</Button>
|
||||
<Button type="submit" variant="primary">Save extension settings</Button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
|
@ -150,7 +149,9 @@
|
|||
</Button>
|
||||
|
||||
{#if rotatedApiKeyError}
|
||||
<p class="mt-3 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-red">
|
||||
<p
|
||||
class="mt-3 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-red"
|
||||
>
|
||||
{rotatedApiKeyError}
|
||||
</p>
|
||||
{/if}
|
||||
|
|
@ -160,7 +161,9 @@
|
|||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
New API key
|
||||
</p>
|
||||
<code class="mt-2 block break-all text-sm text-surface-content">{rotatedApiKey}</code>
|
||||
<code class="mt-2 block break-all text-sm text-surface-content"
|
||||
>{rotatedApiKey}</code
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="surface"
|
||||
|
|
@ -175,15 +178,22 @@
|
|||
</section>
|
||||
|
||||
<section id="user_config_file">
|
||||
<h2 class="text-xl font-semibold text-surface-content">WakaTime Config File</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
WakaTime Config File
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Copy this into your <code class="rounded bg-darker px-1 py-0.5 text-xs">~/.wakatime.cfg</code> file.
|
||||
Copy this into your <code class="rounded bg-darker px-1 py-0.5 text-xs"
|
||||
>~/.wakatime.cfg</code
|
||||
> file.
|
||||
</p>
|
||||
|
||||
{#if config_file.has_api_key && config_file.content}
|
||||
<pre class="mt-4 overflow-x-auto rounded-md border border-surface-200 bg-darker p-4 text-xs text-surface-content">{config_file.content}</pre>
|
||||
<pre
|
||||
class="mt-4 overflow-x-auto rounded-md border border-surface-200 bg-darker p-4 text-xs text-surface-content">{config_file.content}</pre>
|
||||
{:else}
|
||||
<p class="mt-4 rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-muted">
|
||||
<p
|
||||
class="mt-4 rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-muted"
|
||||
>
|
||||
{config_file.empty_message}
|
||||
</p>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@
|
|||
{#if admin_tools.visible}
|
||||
<div class="space-y-8">
|
||||
<section id="wakatime_mirror">
|
||||
<h2 class="text-xl font-semibold text-surface-content">WakaTime Mirrors</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
WakaTime Mirrors
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Mirror heartbeats to external WakaTime-compatible endpoints.
|
||||
</p>
|
||||
|
|
@ -49,7 +51,9 @@
|
|||
<p class="text-sm font-semibold text-surface-content">
|
||||
{mirror.endpoint_url}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted">Last synced: {mirror.last_synced_ago}</p>
|
||||
<p class="mt-1 text-xs text-muted">
|
||||
Last synced: {mirror.last_synced_ago}
|
||||
</p>
|
||||
<form
|
||||
method="post"
|
||||
action={mirror.destroy_path}
|
||||
|
|
@ -61,12 +65,12 @@
|
|||
}}
|
||||
>
|
||||
<input type="hidden" name="_method" value="delete" />
|
||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="surface"
|
||||
size="xs"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="authenticity_token"
|
||||
value={csrfToken}
|
||||
/>
|
||||
<Button type="submit" variant="surface" size="xs">
|
||||
Delete
|
||||
</Button>
|
||||
</form>
|
||||
|
|
@ -75,10 +79,17 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<form method="post" action={paths.user_wakatime_mirrors_path} class="mt-5 space-y-3">
|
||||
<form
|
||||
method="post"
|
||||
action={paths.user_wakatime_mirrors_path}
|
||||
class="mt-5 space-y-3"
|
||||
>
|
||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
<div>
|
||||
<label for="endpoint_url" class="mb-2 block text-sm text-surface-content">
|
||||
<label
|
||||
for="endpoint_url"
|
||||
class="mb-2 block text-sm text-surface-content"
|
||||
>
|
||||
Endpoint URL
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -91,7 +102,10 @@
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="mirror_key" class="mb-2 block text-sm text-surface-content">
|
||||
<label
|
||||
for="mirror_key"
|
||||
class="mb-2 block text-sm text-surface-content"
|
||||
>
|
||||
WakaTime API Key
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -102,17 +116,14 @@
|
|||
class="w-full rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-surface-content focus:border-primary focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
Add mirror
|
||||
</Button>
|
||||
<Button type="submit" variant="primary">Add mirror</Button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-muted">
|
||||
<p
|
||||
class="rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-muted"
|
||||
>
|
||||
You are not authorized to access this section.
|
||||
</p>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@
|
|||
let selectedProject = $state("");
|
||||
|
||||
$effect(() => {
|
||||
if (options.badge_themes.length > 0 && !options.badge_themes.includes(selectedTheme)) {
|
||||
if (
|
||||
options.badge_themes.length > 0 &&
|
||||
!options.badge_themes.includes(selectedTheme)
|
||||
) {
|
||||
selectedTheme = defaultTheme(options.badge_themes);
|
||||
}
|
||||
});
|
||||
|
|
@ -67,7 +70,10 @@
|
|||
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label for="badge_theme" class="mb-2 block text-sm text-surface-content">
|
||||
<label
|
||||
for="badge_theme"
|
||||
class="mb-2 block text-sm text-surface-content"
|
||||
>
|
||||
Theme
|
||||
</label>
|
||||
<select
|
||||
|
|
@ -82,14 +88,22 @@
|
|||
</div>
|
||||
|
||||
<div class="rounded-md border border-surface-200 bg-darker p-4">
|
||||
<img src={badgeUrl()} alt="General coding stats badge preview" class="max-w-full rounded" />
|
||||
<pre class="mt-3 overflow-x-auto text-xs text-surface-content">{badgeUrl()}</pre>
|
||||
<img
|
||||
src={badgeUrl()}
|
||||
alt="General coding stats badge preview"
|
||||
class="max-w-full rounded"
|
||||
/>
|
||||
<pre
|
||||
class="mt-3 overflow-x-auto text-xs text-surface-content">{badgeUrl()}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if badges.projects.length > 0 && badges.project_badge_base_url}
|
||||
<div class="mt-6 border-t border-surface-200 pt-6">
|
||||
<label for="badge_project" class="mb-2 block text-sm text-surface-content">
|
||||
<label
|
||||
for="badge_project"
|
||||
class="mb-2 block text-sm text-surface-content"
|
||||
>
|
||||
Project
|
||||
</label>
|
||||
<select
|
||||
|
|
@ -107,20 +121,24 @@
|
|||
alt="Project stats badge preview"
|
||||
class="max-w-full rounded"
|
||||
/>
|
||||
<pre class="mt-3 overflow-x-auto text-xs text-surface-content">{projectBadgeUrl()}</pre>
|
||||
<pre
|
||||
class="mt-3 overflow-x-auto text-xs text-surface-content">{projectBadgeUrl()}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section id="user_markscribe">
|
||||
<h2 class="text-xl font-semibold text-surface-content">Markscribe Template</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
Markscribe Template
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Use this snippet with markscribe to include your coding stats in a
|
||||
README.
|
||||
</p>
|
||||
<div class="mt-4 rounded-md border border-surface-200 bg-darker p-4">
|
||||
<pre class="overflow-x-auto text-sm text-surface-content">{badges.markscribe_template}</pre>
|
||||
<pre
|
||||
class="overflow-x-auto text-sm text-surface-content">{badges.markscribe_template}</pre>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-muted">
|
||||
Reference:
|
||||
|
|
|
|||
|
|
@ -40,24 +40,23 @@
|
|||
>
|
||||
<div class="space-y-8">
|
||||
<section id="user_migration_assistant">
|
||||
<h2 class="text-xl font-semibold text-surface-content">Migration Assistant</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
Migration Assistant
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Queue migration of heartbeats and API keys from legacy Hackatime.
|
||||
</p>
|
||||
<form method="post" action={paths.migrate_heartbeats_path} class="mt-4">
|
||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
<Button
|
||||
type="submit"
|
||||
class="rounded-md"
|
||||
>
|
||||
Start migration
|
||||
</Button>
|
||||
<Button type="submit" class="rounded-md">Start migration</Button>
|
||||
</form>
|
||||
|
||||
{#if migration.jobs.length > 0}
|
||||
<div class="mt-4 space-y-2">
|
||||
{#each migration.jobs as job}
|
||||
<div class="rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-surface-content">
|
||||
<div
|
||||
class="rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-surface-content"
|
||||
>
|
||||
Job {job.id}: {job.status}
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -69,7 +68,9 @@
|
|||
<h2 class="text-xl font-semibold text-surface-content">Download Data</h2>
|
||||
|
||||
{#if data_export.is_restricted}
|
||||
<p class="mt-3 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-red-200">
|
||||
<p
|
||||
class="mt-3 rounded-md border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-red-200"
|
||||
>
|
||||
Data export is currently restricted for this account.
|
||||
</p>
|
||||
{:else}
|
||||
|
|
@ -79,19 +80,25 @@
|
|||
|
||||
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-md border border-surface-200 bg-darker px-3 py-3">
|
||||
<p class="text-xs uppercase tracking-wide text-muted">Total heartbeats</p>
|
||||
<p class="text-xs uppercase tracking-wide text-muted">
|
||||
Total heartbeats
|
||||
</p>
|
||||
<p class="mt-1 text-lg font-semibold text-surface-content">
|
||||
{data_export.total_heartbeats}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-md border border-surface-200 bg-darker px-3 py-3">
|
||||
<p class="text-xs uppercase tracking-wide text-muted">Total coding time</p>
|
||||
<p class="text-xs uppercase tracking-wide text-muted">
|
||||
Total coding time
|
||||
</p>
|
||||
<p class="mt-1 text-lg font-semibold text-surface-content">
|
||||
{data_export.total_coding_time}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-md border border-surface-200 bg-darker px-3 py-3">
|
||||
<p class="text-xs uppercase tracking-wide text-muted">Last 7 days</p>
|
||||
<p class="text-xs uppercase tracking-wide text-muted">
|
||||
Last 7 days
|
||||
</p>
|
||||
<p class="mt-1 text-lg font-semibold text-surface-content">
|
||||
{data_export.heartbeats_last_7_days}
|
||||
</p>
|
||||
|
|
@ -99,10 +106,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<Button
|
||||
href={paths.export_all_heartbeats_path}
|
||||
class="rounded-md"
|
||||
>
|
||||
<Button href={paths.export_all_heartbeats_path} class="rounded-md">
|
||||
Export all heartbeats
|
||||
</Button>
|
||||
|
||||
|
|
@ -123,11 +127,7 @@
|
|||
required
|
||||
class="rounded-md border border-surface-200 bg-surface px-3 py-2 text-sm text-surface-content focus:border-primary focus:outline-none"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="surface"
|
||||
class="rounded-md"
|
||||
>
|
||||
<Button type="submit" variant="surface" class="rounded-md">
|
||||
Export date range
|
||||
</Button>
|
||||
</form>
|
||||
|
|
@ -141,7 +141,10 @@
|
|||
class="mt-4 rounded-md border border-surface-200 bg-darker p-4"
|
||||
>
|
||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
<label class="mb-2 block text-sm text-surface-content" for="heartbeat_file">
|
||||
<label
|
||||
class="mb-2 block text-sm text-surface-content"
|
||||
for="heartbeat_file"
|
||||
>
|
||||
Import heartbeats (development only)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -152,11 +155,7 @@
|
|||
required
|
||||
class="w-full rounded-md border border-surface-200 bg-surface px-3 py-2 text-sm text-surface-content"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="surface"
|
||||
class="mt-3 rounded-md"
|
||||
>
|
||||
<Button type="submit" variant="surface" class="mt-3 rounded-md">
|
||||
Import file
|
||||
</Button>
|
||||
</form>
|
||||
|
|
@ -165,11 +164,13 @@
|
|||
</section>
|
||||
|
||||
<section id="delete_account">
|
||||
<h2 class="text-xl font-semibold text-surface-content">Account Deletion</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
Account Deletion
|
||||
</h2>
|
||||
{#if user.can_request_deletion}
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Request permanent deletion. The account enters a waiting period
|
||||
before final removal.
|
||||
Request permanent deletion. The account enters a waiting period before
|
||||
final removal.
|
||||
</p>
|
||||
<form
|
||||
method="post"
|
||||
|
|
@ -186,16 +187,14 @@
|
|||
}}
|
||||
>
|
||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="surface"
|
||||
class="rounded-md"
|
||||
>
|
||||
<Button type="submit" variant="surface" class="rounded-md">
|
||||
Request deletion
|
||||
</Button>
|
||||
</form>
|
||||
{:else}
|
||||
<p class="mt-3 rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-muted">
|
||||
<p
|
||||
class="mt-3 rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-muted"
|
||||
>
|
||||
Deletion request is unavailable for this account right now.
|
||||
</p>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@
|
|||
>
|
||||
<div class="space-y-8">
|
||||
<section id="user_slack_status">
|
||||
<h2 class="text-xl font-semibold text-surface-content">Slack Status Sync</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
Slack Status Sync
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Keep your Slack status updated while you are actively coding.
|
||||
</p>
|
||||
|
|
@ -76,10 +78,14 @@
|
|||
</section>
|
||||
|
||||
<section id="user_slack_notifications">
|
||||
<h2 class="text-xl font-semibold text-surface-content">Slack Channel Notifications</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
Slack Channel Notifications
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Enable notifications in any channel by running
|
||||
<code class="rounded bg-darker px-1 py-0.5 text-xs text-surface-content">
|
||||
<code
|
||||
class="rounded bg-darker px-1 py-0.5 text-xs text-surface-content"
|
||||
>
|
||||
/sailorslog on
|
||||
</code>
|
||||
in that channel.
|
||||
|
|
@ -88,26 +94,36 @@
|
|||
{#if slack.notification_channels.length > 0}
|
||||
<ul class="mt-4 space-y-2">
|
||||
{#each slack.notification_channels as channel}
|
||||
<li class="rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-surface-content">
|
||||
<a href={channel.url} target="_blank" class="underline">{channel.label}</a>
|
||||
<li
|
||||
class="rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-surface-content"
|
||||
>
|
||||
<a href={channel.url} target="_blank" class="underline"
|
||||
>{channel.label}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="mt-4 rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-muted">
|
||||
<p
|
||||
class="mt-4 rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-muted"
|
||||
>
|
||||
No channel notifications are enabled.
|
||||
</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section id="user_github_account">
|
||||
<h2 class="text-xl font-semibold text-surface-content">Connected GitHub Account</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
Connected GitHub Account
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Connect GitHub to show project links in dashboards and leaderboards.
|
||||
</p>
|
||||
|
||||
{#if github.connected && github.username}
|
||||
<div class="mt-4 rounded-md border border-surface-200 bg-darker px-3 py-3 text-sm text-surface-content">
|
||||
<div
|
||||
class="mt-4 rounded-md border border-surface-200 bg-darker px-3 py-3 text-sm text-surface-content"
|
||||
>
|
||||
Connected as
|
||||
<a href={github.profile_url || "#"} target="_blank" class="underline">
|
||||
@{github.username}
|
||||
|
|
@ -135,11 +151,7 @@
|
|||
>
|
||||
<input type="hidden" name="_method" value="delete" />
|
||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="surface"
|
||||
class="rounded-md"
|
||||
>
|
||||
<Button type="submit" variant="surface" class="rounded-md">
|
||||
Unlink GitHub
|
||||
</Button>
|
||||
</form>
|
||||
|
|
@ -155,7 +167,9 @@
|
|||
</section>
|
||||
|
||||
<section id="user_email_addresses">
|
||||
<h2 class="text-xl font-semibold text-surface-content">Email Addresses</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
Email Addresses
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Add or remove email addresses used for sign-in and verification.
|
||||
</p>
|
||||
|
|
@ -163,7 +177,9 @@
|
|||
<div class="mt-4 space-y-2">
|
||||
{#if emails.length > 0}
|
||||
{#each emails as email}
|
||||
<div class="flex flex-wrap items-center gap-2 rounded-md border border-surface-200 bg-darker px-3 py-2">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 rounded-md border border-surface-200 bg-darker px-3 py-2"
|
||||
>
|
||||
<div class="grow text-sm text-surface-content">
|
||||
<p>{email.email}</p>
|
||||
<p class="text-xs text-muted">{email.source}</p>
|
||||
|
|
@ -171,7 +187,11 @@
|
|||
{#if email.can_unlink}
|
||||
<form method="post" action={paths.unlink_email_path}>
|
||||
<input type="hidden" name="_method" value="delete" />
|
||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="authenticity_token"
|
||||
value={csrfToken}
|
||||
/>
|
||||
<input type="hidden" name="email" value={email.email} />
|
||||
<Button
|
||||
type="submit"
|
||||
|
|
@ -186,13 +206,19 @@
|
|||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-muted">
|
||||
<p
|
||||
class="rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-muted"
|
||||
>
|
||||
No email addresses are linked.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<form method="post" action={paths.add_email_path} class="mt-4 flex flex-col gap-3 sm:flex-row">
|
||||
<form
|
||||
method="post"
|
||||
action={paths.add_email_path}
|
||||
class="mt-4 flex flex-col gap-3 sm:flex-row"
|
||||
>
|
||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
<input
|
||||
type="email"
|
||||
|
|
@ -201,12 +227,7 @@
|
|||
placeholder="name@example.com"
|
||||
class="grow rounded-md border border-surface-200 bg-darker px-3 py-2 text-sm text-surface-content focus:border-primary focus:outline-none"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
class="rounded-md"
|
||||
>
|
||||
Add email
|
||||
</Button>
|
||||
<Button type="submit" class="rounded-md">Add email</Button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
}: ProfilePageProps = $props();
|
||||
|
||||
let csrfToken = $state("");
|
||||
let selectedTheme = $state(user.theme || "gruvbox_dark");
|
||||
let selectedTheme = $state("gruvbox_dark");
|
||||
|
||||
onMount(() => {
|
||||
csrfToken =
|
||||
|
|
@ -28,6 +28,10 @@
|
|||
.querySelector("meta[name='csrf-token']")
|
||||
?.getAttribute("content") || "";
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
selectedTheme = user.theme || "gruvbox_dark";
|
||||
});
|
||||
</script>
|
||||
|
||||
<SettingsShell
|
||||
|
|
@ -41,7 +45,9 @@
|
|||
>
|
||||
<div class="space-y-8">
|
||||
<section id="user_region">
|
||||
<h2 class="text-xl font-semibold text-surface-content">Region and Timezone</h2>
|
||||
<h2 class="text-xl font-semibold text-surface-content">
|
||||
Region and Timezone
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Use your local region and timezone for accurate dashboards and
|
||||
leaderboards.
|
||||
|
|
@ -51,7 +57,10 @@
|
|||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
|
||||
<div>
|
||||
<label for="country_code" class="mb-2 block text-sm text-surface-content">
|
||||
<label
|
||||
for="country_code"
|
||||
class="mb-2 block text-sm text-surface-content"
|
||||
>
|
||||
Country
|
||||
</label>
|
||||
<select
|
||||
|
|
@ -83,12 +92,7 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
Save region settings
|
||||
</Button>
|
||||
<Button type="submit" variant="primary">Save region settings</Button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
|
@ -118,12 +122,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
Save username
|
||||
</Button>
|
||||
<Button type="submit" variant="primary">Save username</Button>
|
||||
</form>
|
||||
|
||||
{#if badges.profile_url}
|
||||
|
|
@ -150,7 +149,11 @@
|
|||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||
|
||||
<label class="flex items-center gap-3 text-sm text-surface-content">
|
||||
<input type="hidden" name="user[allow_public_stats_lookup]" value="0" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="user[allow_public_stats_lookup]"
|
||||
value="0"
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="user[allow_public_stats_lookup]"
|
||||
|
|
@ -161,12 +164,7 @@
|
|||
Allow public stats lookup
|
||||
</label>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
Save privacy settings
|
||||
</Button>
|
||||
<Button type="submit" variant="primary">Save privacy settings</Button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
|
@ -200,11 +198,15 @@
|
|||
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-surface-content">{theme.label}</p>
|
||||
<p class="text-sm font-semibold text-surface-content">
|
||||
{theme.label}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted">{theme.description}</p>
|
||||
</div>
|
||||
{#if isSelected}
|
||||
<span class="rounded-full bg-primary/20 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
<span
|
||||
class="rounded-full bg-primary/20 px-2 py-0.5 text-xs font-medium text-primary"
|
||||
>
|
||||
Selected
|
||||
</span>
|
||||
{/if}
|
||||
|
|
@ -223,26 +225,36 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-2 grid grid-cols-[1fr_auto] items-center gap-2">
|
||||
<span class="h-2 rounded" style={`background:${theme.preview.primary};`}></span>
|
||||
<span class="h-2 w-8 rounded" style={`background:${theme.preview.darkless};`}></span>
|
||||
<span
|
||||
class="h-2 rounded"
|
||||
style={`background:${theme.preview.primary};`}
|
||||
></span>
|
||||
<span
|
||||
class="h-2 w-8 rounded"
|
||||
style={`background:${theme.preview.darkless};`}
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex gap-1.5">
|
||||
<span class="h-1.5 w-6 rounded" style={`background:${theme.preview.info};`}></span>
|
||||
<span class="h-1.5 w-6 rounded" style={`background:${theme.preview.success};`}></span>
|
||||
<span class="h-1.5 w-6 rounded" style={`background:${theme.preview.warning};`}></span>
|
||||
<span
|
||||
class="h-1.5 w-6 rounded"
|
||||
style={`background:${theme.preview.info};`}
|
||||
></span>
|
||||
<span
|
||||
class="h-1.5 w-6 rounded"
|
||||
style={`background:${theme.preview.success};`}
|
||||
></span>
|
||||
<span
|
||||
class="h-1.5 w-6 rounded"
|
||||
style={`background:${theme.preview.warning};`}
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
Save theme
|
||||
</Button>
|
||||
<Button type="submit" variant="primary">Save theme</Button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@
|
|||
}: SettingsCommonProps & { children?: Snippet } = $props();
|
||||
|
||||
const sections = $derived(buildSections(section_paths, admin_tools.visible));
|
||||
const knownSectionIds = $derived(new Set(sections.map((section) => section.id)));
|
||||
const knownSectionIds = $derived(
|
||||
new Set(sections.map((section) => section.id)),
|
||||
);
|
||||
|
||||
const sectionButtonClass = (sectionId: keyof SectionPaths) =>
|
||||
`block w-full px-4 py-4 text-left transition-colors ${
|
||||
|
|
@ -32,7 +34,9 @@
|
|||
if (!section || !knownSectionIds.has(section)) return;
|
||||
if (section === active_section || !section_paths[section]) return;
|
||||
|
||||
window.location.replace(`${section_paths[section]}${window.location.hash}`);
|
||||
window.location.replace(
|
||||
`${section_paths[section]}${window.location.hash}`,
|
||||
);
|
||||
};
|
||||
|
||||
syncSectionFromHash();
|
||||
|
|
@ -52,7 +56,9 @@
|
|||
</header>
|
||||
|
||||
{#if errors.full_messages.length > 0}
|
||||
<div class="mb-6 rounded-lg border border-danger/40 bg-danger/10 px-4 py-3 text-sm text-red">
|
||||
<div
|
||||
class="mb-6 rounded-lg border border-danger/40 bg-danger/10 px-4 py-3 text-sm text-red"
|
||||
>
|
||||
<p class="font-semibold">Some changes could not be saved:</p>
|
||||
<ul class="mt-2 list-disc pl-5">
|
||||
{#each errors.full_messages as message}
|
||||
|
|
@ -64,7 +70,9 @@
|
|||
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-[260px_minmax(0,1fr)]">
|
||||
<aside class="h-max lg:sticky lg:top-8">
|
||||
<div class="overflow-hidden rounded-xl border border-surface-200 bg-surface divide-y divide-surface-200">
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-surface-200 bg-surface divide-y divide-surface-200"
|
||||
>
|
||||
{#each sections as section}
|
||||
<Link href={section.path} class={sectionButtonClass(section.id)}>
|
||||
<p class="text-sm font-semibold">{section.label}</p>
|
||||
|
|
|
|||
|
|
@ -112,15 +112,14 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div class="text-center py-2">
|
||||
<h4 class="text-xl font-bold text-surface-content">Setup complete!</h4>
|
||||
<h4 class="text-xl font-bold text-surface-content">
|
||||
Setup complete!
|
||||
</h4>
|
||||
<p class="text-secondary text-sm mb-6">
|
||||
Heartbeat detected {heartbeatTimeAgo}.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
href="/my/wakatime_setup/step-2"
|
||||
size="lg"
|
||||
>
|
||||
<Button href="/my/wakatime_setup/step-2" size="lg">
|
||||
Continue to Step 2 →
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -225,6 +224,7 @@
|
|||
class="mt-4 rounded-lg overflow-hidden border border-darkless"
|
||||
>
|
||||
<iframe
|
||||
title="macOS setup video tutorial"
|
||||
width="100%"
|
||||
height="300"
|
||||
src="https://www.youtube.com/embed/QTwhJy7nT_w?modestbranding=1&rel=0"
|
||||
|
|
@ -312,6 +312,7 @@
|
|||
class="mt-4 rounded-lg overflow-hidden border border-darkless"
|
||||
>
|
||||
<iframe
|
||||
title="Windows setup video tutorial"
|
||||
width="100%"
|
||||
height="300"
|
||||
src="https://www.youtube.com/embed/fX9tsiRvzhg?modestbranding=1&rel=0"
|
||||
|
|
|
|||
|
|
@ -1,17 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { Link } from '@inertiajs/svelte';
|
||||
import Stepper from './Stepper.svelte';
|
||||
import { Link } from "@inertiajs/svelte";
|
||||
import Stepper from "./Stepper.svelte";
|
||||
|
||||
const editors = [
|
||||
{ id: 'vscode', name: 'VS Code', icon: '/images/editor-icons/vs-code-128.png' },
|
||||
{ id: 'vim', name: 'Vim', icon: '/images/editor-icons/vim-128.png' },
|
||||
{ id: 'neovim', name: 'Neovim', icon: '/images/editor-icons/neovim-128.png' },
|
||||
{ id: 'emacs', name: 'Emacs', icon: '/images/editor-icons/emacs-128.png' },
|
||||
{ id: 'jetbrains', name: 'JetBrains', icon: '/images/editor-icons/jetbrains-128.png' },
|
||||
{ id: 'sublime', name: 'Sublime', icon: '/images/editor-icons/sublime-text-128.png' },
|
||||
{ id: 'unity', name: 'Unity', icon: '/images/editor-icons/unity-128.png' },
|
||||
{ id: 'godot', name: 'Godot', icon: '/images/editor-icons/godot-128.png' },
|
||||
{ id: 'other', name: 'Other', icon: null, emoji: '🔧' }
|
||||
{
|
||||
id: "vscode",
|
||||
name: "VS Code",
|
||||
icon: "/images/editor-icons/vs-code-128.png",
|
||||
},
|
||||
{ id: "vim", name: "Vim", icon: "/images/editor-icons/vim-128.png" },
|
||||
{
|
||||
id: "neovim",
|
||||
name: "Neovim",
|
||||
icon: "/images/editor-icons/neovim-128.png",
|
||||
},
|
||||
{ id: "emacs", name: "Emacs", icon: "/images/editor-icons/emacs-128.png" },
|
||||
{
|
||||
id: "jetbrains",
|
||||
name: "JetBrains",
|
||||
icon: "/images/editor-icons/jetbrains-128.png",
|
||||
},
|
||||
{
|
||||
id: "sublime",
|
||||
name: "Sublime",
|
||||
icon: "/images/editor-icons/sublime-text-128.png",
|
||||
},
|
||||
{ id: "unity", name: "Unity", icon: "/images/editor-icons/unity-128.png" },
|
||||
{ id: "godot", name: "Godot", icon: "/images/editor-icons/godot-128.png" },
|
||||
{ id: "other", name: "Other", icon: null, emoji: "🔧" },
|
||||
];
|
||||
</script>
|
||||
|
||||
|
|
@ -25,22 +41,36 @@
|
|||
|
||||
<div class="text-center mb-10">
|
||||
<h2 class="text-2xl font-bold mb-2">Choose your editor</h2>
|
||||
<p class="text-secondary">Select the editor you'll be using. You can set up more later!</p>
|
||||
<p class="text-secondary">
|
||||
Select the editor you'll be using. You can set up more later!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||
{#each editors as editor}
|
||||
<Link
|
||||
href={`/my/wakatime_setup/step-3?editor=${editor.id}`}
|
||||
class="group flex flex-col items-center justify-center p-6 bg-dark border border-darkless rounded-xl hover:border-primary transition-all duration-200 hover:shadow-lg hover:shadow-primary/10">
|
||||
<div class="w-16 h-16 mb-4 flex items-center justify-center transition-transform duration-200 group-hover:scale-110">
|
||||
class="group flex flex-col items-center justify-center p-6 bg-dark border border-darkless rounded-xl hover:border-primary transition-all duration-200 hover:shadow-lg hover:shadow-primary/10"
|
||||
>
|
||||
<div
|
||||
class="w-16 h-16 mb-4 flex items-center justify-center transition-transform duration-200 group-hover:scale-110"
|
||||
>
|
||||
{#if editor.icon}
|
||||
<img src={editor.icon} alt={editor.name} class="w-12 h-12 object-contain">
|
||||
<img
|
||||
src={editor.icon}
|
||||
alt={editor.name}
|
||||
class="w-12 h-12 object-contain"
|
||||
/>
|
||||
{:else}
|
||||
<div class="w-12 h-12 flex items-center justify-center text-3xl">{editor.emoji}</div>
|
||||
<div class="w-12 h-12 flex items-center justify-center text-3xl">
|
||||
{editor.emoji}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="font-medium text-surface-content group-hover:text-primary transition-colors">{editor.name}</span>
|
||||
<span
|
||||
class="font-medium text-surface-content group-hover:text-primary transition-colors"
|
||||
>{editor.name}</span
|
||||
>
|
||||
</Link>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -223,7 +223,8 @@
|
|||
</summary>
|
||||
<div class="mt-4 pl-6">
|
||||
<p class="text-sm mb-3 text-secondary">
|
||||
You'll see a clock icon and time spent coding in your status bar:
|
||||
You'll see a clock icon and time spent coding in your status
|
||||
bar:
|
||||
</p>
|
||||
<img
|
||||
src="/images/editor-toolbars/vs-code.png"
|
||||
|
|
@ -349,11 +350,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
href="/my/wakatime_setup/step-4"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
>
|
||||
<Button href="/my/wakatime_setup/step-4" size="xl" class="w-full">
|
||||
Next Step
|
||||
</Button>
|
||||
{:else if editor === "jetbrains"}
|
||||
|
|
@ -367,7 +364,8 @@
|
|||
<div>
|
||||
<h3 class="text-xl font-semibold">Set Up JetBrains IDEs</h3>
|
||||
<p class="text-secondary text-sm">
|
||||
Install the WakaTime extension for JetBrains IDEs (like IntelliJ and PyCharm).
|
||||
Install the WakaTime extension for JetBrains IDEs (like IntelliJ
|
||||
and PyCharm).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -386,7 +384,9 @@
|
|||
<div>
|
||||
<p class="font-medium mb-1">Open Settings</p>
|
||||
<p class="text-sm text-secondary">
|
||||
Open your IDE and go to <b>Settings</b> (Ctrl+Alt+S on Windows/Linux, Command+, on macOS), <b>Plugins</b>, then <b>Marketplace</b>.
|
||||
Open your IDE and go to <b>Settings</b> (Ctrl+Alt+S on
|
||||
Windows/Linux, Command+, on macOS), <b>Plugins</b>, then
|
||||
<b>Marketplace</b>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -403,11 +403,11 @@
|
|||
Search for <b>WakaTime</b> in the marketplace and click Install.
|
||||
|
||||
<a
|
||||
href="https://plugins.jetbrains.com/plugin/7425-wakatime"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-cyan hover:underline">View on Marketplace</a
|
||||
>
|
||||
href="https://plugins.jetbrains.com/plugin/7425-wakatime"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-cyan hover:underline">View on Marketplace</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -449,7 +449,8 @@
|
|||
</summary>
|
||||
<div class="mt-4 pl-6">
|
||||
<p class="text-sm mb-3 text-secondary">
|
||||
You'll see a WakaTime icon and time spent coding in your status bar:
|
||||
You'll see a WakaTime icon and time spent coding in your
|
||||
status bar:
|
||||
</p>
|
||||
<img
|
||||
src="/images/editor-toolbars/jetbrains.png"
|
||||
|
|
@ -462,11 +463,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
href="/my/wakatime_setup/step-4"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
>
|
||||
<Button href="/my/wakatime_setup/step-4" size="xl" class="w-full">
|
||||
Next Step
|
||||
</Button>
|
||||
{:else if editor === "sublime"}
|
||||
|
|
@ -515,13 +512,17 @@
|
|||
<div>
|
||||
<p class="font-medium mb-1">Install WakaTime Plugin</p>
|
||||
<p class="text-sm text-secondary">
|
||||
Open the Command Palette (Ctrl+Shift+P on Windows/Linux, Command+Shift+P on macOS), type <b>Package Control: Install Package</b>, and press Enter. Then type <b>WakaTime</b> and press Enter to install.
|
||||
<a
|
||||
href="https://packagecontrol.io/packages/WakaTime"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-cyan hover:underline">View on Package Control</a
|
||||
>
|
||||
Open the Command Palette (Ctrl+Shift+P on Windows/Linux,
|
||||
Command+Shift+P on macOS), type <b
|
||||
>Package Control: Install Package</b
|
||||
>, and press Enter. Then type <b>WakaTime</b> and press Enter to
|
||||
install.
|
||||
<a
|
||||
href="https://packagecontrol.io/packages/WakaTime"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-cyan hover:underline">View on Package Control</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -535,7 +536,8 @@
|
|||
<div>
|
||||
<p class="font-medium mb-1">Start Coding</p>
|
||||
<p class="text-sm text-secondary">
|
||||
After installing WakaTime, open any file and start typing to send your first heartbeat.
|
||||
After installing WakaTime, open any file and start typing to
|
||||
send your first heartbeat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -562,7 +564,8 @@
|
|||
</summary>
|
||||
<div class="mt-4 pl-6">
|
||||
<p class="text-sm mb-3 text-secondary">
|
||||
You'll see your time spent coding in your status bar, which looks something like <code>Today: 1h 23m</code>.
|
||||
You'll see your time spent coding in your status bar, which
|
||||
looks something like <code>Today: 1h 23m</code>.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
|
@ -570,11 +573,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
href="/my/wakatime_setup/step-4"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
>
|
||||
<Button href="/my/wakatime_setup/step-4" size="xl" class="w-full">
|
||||
Next Step
|
||||
</Button>
|
||||
{:else if editorData[editor]}
|
||||
|
|
@ -601,7 +600,9 @@
|
|||
<div class="pt-6 border-t border-darkless"></div>
|
||||
{/if}
|
||||
<div>
|
||||
<h4 class="text-sm font-medium mb-2 text-surface-content">{method.name}</h4>
|
||||
<h4 class="text-sm font-medium mb-2 text-surface-content">
|
||||
{method.name}
|
||||
</h4>
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="bg-darker border border-darkless rounded-lg overflow-x-auto"
|
||||
|
|
@ -620,11 +621,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
href="/my/wakatime_setup/step-4"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
>
|
||||
<Button href="/my/wakatime_setup/step-4" size="xl" class="w-full">
|
||||
Next Step
|
||||
</Button>
|
||||
{:else}
|
||||
|
|
@ -697,11 +694,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
href="/my/wakatime_setup/step-4"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
>
|
||||
<Button href="/my/wakatime_setup/step-4" size="xl" class="w-full">
|
||||
Next Step
|
||||
</Button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -9,29 +9,49 @@
|
|||
{ number: 1, label: "Install" },
|
||||
{ number: 2, label: "Editor" },
|
||||
{ number: 3, label: "Plugin" },
|
||||
{ number: 4, label: "Finish" }
|
||||
{ number: 4, label: "Finish" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="mb-10">
|
||||
<div class="relative flex items-center justify-between w-full max-w-2xl mx-auto">
|
||||
<div
|
||||
class="relative flex items-center justify-between w-full max-w-2xl mx-auto"
|
||||
>
|
||||
<div class="absolute top-5 left-0 w-full h-0.5 bg-darkless -z-10"></div>
|
||||
|
||||
{#each steps as step}
|
||||
<div class="flex flex-col items-center gap-2 bg-darker z-10 px-2">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold border-2 transition-colors duration-200
|
||||
{currentStep > step.number ? 'bg-green border-green text-darker' :
|
||||
currentStep === step.number ? 'bg-primary border-primary text-on-primary' :
|
||||
'bg-dark border-darkless text-secondary'}">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold border-2 transition-colors duration-200
|
||||
{currentStep > step.number
|
||||
? 'bg-green border-green text-darker'
|
||||
: currentStep === step.number
|
||||
? 'bg-primary border-primary text-on-primary'
|
||||
: 'bg-dark border-darkless text-secondary'}"
|
||||
>
|
||||
{#if currentStep > step.number}
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
{step.number}
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-xs font-medium {currentStep === step.number ? 'text-surface-content' : 'text-secondary'}">{step.label}</span>
|
||||
<span
|
||||
class="text-xs font-medium {currentStep === step.number
|
||||
? 'text-surface-content'
|
||||
: 'text-secondary'}">{step.label}</span
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
<div class="filter flex-1 min-w-37.5 relative">
|
||||
<label class="filter-label block text-xs font-medium mb-1.5 text-secondary/80 uppercase tracking-wider">Date Range</label>
|
||||
<div class="custom-select relative w-full" id="interval-select" data-param="interval">
|
||||
<div class="select-header-container group flex items-center border border-surface-content/10 rounded-lg bg-darkless m-0 p-0 transition-all duration-200 hover:border-surface-content/20">
|
||||
<div class="select-header flex-1 px-3 py-2.5 text-sm cursor-pointer select-none text-muted m-0 bg-transparent flex items-center justify-between" id="interval-header">
|
||||
<span><%= human_interval_name(selected_interval, from: selected_from, to: selected_to) %></span>
|
||||
<svg class="w-4 h-4 text-secondary/60 transition-transform duration-200 group-hover:text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</div>
|
||||
<button class="clear-button px-2.5 py-2 text-sm leading-none text-secondary/60 bg-transparent border-0 border-l border-surface-content/10 cursor-pointer m-0 hover:text-red hover:bg-red/10 transition-colors duration-150 <%= selected_interval.blank? && selected_from.blank? && selected_to.blank? ? 'hidden' : '' %>" id="interval-clear-button">×</button>
|
||||
</div>
|
||||
<div class="options-container absolute top-full left-0 right-0 min-w-64 bg-darkless border border-surface-content/10 rounded-lg mt-2 shadow-xl shadow-black/50 z-1000 p-2" id="interval-options" style="display: none;">
|
||||
<div class="options-list overflow-y-auto m-0 max-h-56 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
|
||||
<% TimeRangeFilterable::RANGES.each do |key, config| %>
|
||||
<label class="option flex items-center px-3 py-2.5 cursor-pointer text-sm text-muted m-0 bg-transparent rounded-md hover:bg-dark transition-colors duration-150" data-interval-key="<%= key %>" data-interval-label="<%= config[:human_name] %>">
|
||||
<input type="radio" name="interval-option" class="mr-3 mb-0 h-4 w-4 min-w-4 appearance-none border border-surface-content/20 rounded-full bg-dark relative cursor-pointer p-0 checked:bg-primary checked:border-primary hover:border-surface-content/40 transition-colors duration-150" value="<%= key %>" <%= 'checked' if selected_interval == key.to_s %>>
|
||||
<span><%= config[:human_name] %></span>
|
||||
</label>
|
||||
<% end %>
|
||||
<label class="option flex items-center px-3 py-2.5 cursor-pointer text-sm text-muted m-0 bg-transparent rounded-md hover:bg-dark transition-colors duration-150" data-interval-key="" data-interval-label="All Time">
|
||||
<input type="radio" name="interval-option" class="mr-3 mb-0 h-4 w-4 min-w-4 appearance-none border border-surface-content/20 rounded-full bg-dark relative cursor-pointer p-0 checked:bg-primary checked:border-primary hover:border-surface-content/40 transition-colors duration-150" value="" <%= 'checked' if selected_interval.blank? && selected_from.blank? && selected_to.blank? %>>
|
||||
<span>All Time</span>
|
||||
</label>
|
||||
</div>
|
||||
<hr class="my-2 border-surface-content/10">
|
||||
<div class="flex flex-col gap-2.5 pt-1">
|
||||
<label class="flex items-center justify-between text-sm text-muted">
|
||||
<span class="text-secondary/80">Start</span>
|
||||
<input type="date" class="ml-2 py-2 px-3 bg-dark border border-surface-content/10 rounded-md text-sm text-muted focus:outline-none focus:border-surface-content/20 transition-colors duration-150" id="interval-custom-start" value="<%= selected_from %>">
|
||||
</label>
|
||||
<label class="flex items-center justify-between text-sm text-muted">
|
||||
<span class="text-secondary/80">End</span>
|
||||
<input type="date" class="ml-2 py-2 px-3 bg-dark border border-surface-content/10 rounded-md text-sm text-muted focus:outline-none focus:border-surface-content/20 transition-colors duration-150" id="interval-custom-end" value="<%= selected_to %>">
|
||||
</label>
|
||||
<button type="button" class="px-3 py-2.5 mt-1 rounded-md font-medium text-sm transition-all duration-200 cursor-pointer bg-primary text-on-primary hover:bg-primary/90" id="interval-apply-custom">Apply Custom Range</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<div class="filter flex-1 min-w-37.5 relative">
|
||||
<label class="filter-label block text-xs font-medium mb-1.5 text-secondary/80 uppercase tracking-wider"><%= label %></label>
|
||||
<div class="custom-select relative w-full" id="<%= param %>-select" data-param="<%= param %>">
|
||||
<div class="select-header-container group flex items-center border border-surface-content/10 rounded-lg bg-darkless m-0 p-0 transition-all duration-200 hover:border-surface-content/20">
|
||||
<div class="select-header flex-1 px-3 py-2.5 text-sm cursor-pointer select-none text-muted m-0 bg-transparent flex items-center justify-between">
|
||||
<span>Filter by <%= label.downcase %>...</span>
|
||||
<svg class="w-4 h-4 text-secondary/60 transition-transform duration-200 group-hover:text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</div>
|
||||
<button class="clear-button px-2.5 py-2 text-sm leading-none text-secondary/60 bg-transparent border-0 border-l border-surface-content/10 cursor-pointer m-0 hover:text-red hover:bg-red/10 transition-colors duration-150 hidden">×</button>
|
||||
</div>
|
||||
<div class="options-container absolute top-full left-0 right-0 min-w-64 bg-darkless border border-surface-content/10 rounded-lg mt-2 shadow-xl shadow-black/50 z-1000 p-2 hidden">
|
||||
<input type="text" class="search-input w-full border border-surface-content/10 px-3 py-2.5 mb-2 bg-dark text-surface-content text-sm rounded-md h-auto placeholder:text-secondary/60 focus:outline-none focus:border-surface-content/20" placeholder="Search <%= label.downcase %>...">
|
||||
<div class="options-list overflow-y-auto m-0 max-h-64 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
|
||||
<% values.reject(&:blank?).each do |value| %>
|
||||
<label class="option flex items-center px-3 py-2.5 cursor-pointer text-sm text-muted m-0 bg-transparent rounded-md hover:bg-dark transition-colors duration-150">
|
||||
<input type="checkbox" class="mr-3 mb-0 h-4 w-4 min-w-4 appearance-none border border-surface-content/20 rounded bg-dark relative cursor-pointer p-0 checked:bg-primary checked:border-primary hover:border-surface-content/40 transition-colors duration-150" value="<%= value %>" <%= 'checked' if selected&.include?(value) %>>
|
||||
<span><%= value %></span>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,410 +0,0 @@
|
|||
<%= turbo_frame_tag "filterable_dashboard" do %>
|
||||
<div class="max-w-6xl mx-auto my-0">
|
||||
<div class="flex gap-4 mt-2 mb-6 flex-wrap">
|
||||
<%= render partial: "shared/dashboard_interval_selector", locals: { selected_interval: @selected_interval, selected_from: @selected_from, selected_to: @selected_to } %>
|
||||
<%= render partial: "shared/multi_select", locals: { label: "Project", param: "project", values: @project, selected: @selected_project } %>
|
||||
<%= render partial: "shared/multi_select", locals: { label: "Language", param: "language", values: @language, selected: @selected_language } %>
|
||||
<%= render partial: "shared/multi_select", locals: { label: "OS", param: "operating_system", values: @operating_system, selected: @selected_operating_system } %>
|
||||
<%= render partial: "shared/multi_select", locals: { label: "Editor", param: "editor", values: @editor, selected: @selected_editor } %>
|
||||
<%= render partial: "shared/multi_select", locals: { label: "Category", param: "category", values: @category, selected: @selected_category } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filterable_dashboard_content">
|
||||
<%= render partial: "filterable_dashboard_content" %>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// UI only: handle dropdown behavior, clear buttons, etc.
|
||||
window.initializeMultiSelect =
|
||||
window.initializeMultiSelect ||
|
||||
function (selectId) {
|
||||
const select = document.getElementById(selectId);
|
||||
if (!select || select.dataset.initialized) return;
|
||||
|
||||
select.dataset.initialized = "true";
|
||||
const header = select.querySelector(".select-header");
|
||||
const container = select.querySelector(".options-container");
|
||||
const checkboxes = select.querySelectorAll('input[type="checkbox"]');
|
||||
const clearButton = select.querySelector(".clear-button");
|
||||
const searchInput = select.querySelector(".search-input");
|
||||
|
||||
// Header and clear button visibility
|
||||
const checkedBoxes = Array.from(checkboxes).filter((cb) => cb.checked);
|
||||
const headerSpanInit = header.querySelector("span") || header;
|
||||
if (checkedBoxes.length > 0 && clearButton) {
|
||||
clearButton.style.display = "block";
|
||||
if (checkedBoxes.length === 1) {
|
||||
headerSpanInit.textContent = checkedBoxes[0].value;
|
||||
} else {
|
||||
headerSpanInit.textContent = `${checkedBoxes.length} selected`;
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle dropdown
|
||||
header.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
const isVisible = container.style.display === "block";
|
||||
document.querySelectorAll(".options-container").forEach((c) => {
|
||||
if (c !== container) c.style.display = "none";
|
||||
});
|
||||
container.style.display = isVisible ? "none" : "block";
|
||||
if (!isVisible && searchInput) searchInput.focus();
|
||||
});
|
||||
|
||||
// Clear filter
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
checkboxes.forEach((cb) => (cb.checked = false));
|
||||
updateSelect(select);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle search input
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", function (e) {
|
||||
const searchTerm = e.target.value.toLowerCase().trim();
|
||||
const options = select.querySelectorAll(".option");
|
||||
options.forEach((option) => {
|
||||
const text = option.querySelector("span").textContent.toLowerCase().trim();
|
||||
option.style.display = text.includes(searchTerm) ? "" : "none";
|
||||
});
|
||||
});
|
||||
searchInput.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
// Update header text and URL when checkboxes change
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener("change", function () {
|
||||
updateSelect(select);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.updateSelect =
|
||||
window.updateSelect ||
|
||||
function (select) {
|
||||
const header = select.querySelector(".select-header");
|
||||
const clearButton = select.querySelector(".clear-button");
|
||||
const checkboxes = select.querySelectorAll('input[type="checkbox"]');
|
||||
const param = select.dataset.param;
|
||||
const frame = document.querySelector("#filterable_dashboard_content");
|
||||
frame.classList.add("loading");
|
||||
|
||||
const selected = Array.from(checkboxes)
|
||||
.filter((cb) => cb.checked)
|
||||
.map((cb) => cb.value);
|
||||
|
||||
// Header text, clear button
|
||||
const headerSpan = header.querySelector("span") || header;
|
||||
if (selected.length === 0) {
|
||||
headerSpan.textContent = `Filter by ${header.closest(".filter").querySelector(".filter-label").textContent.toLowerCase()}...`;
|
||||
if (clearButton) clearButton.style.display = "none";
|
||||
} else if (selected.length === 1) {
|
||||
headerSpan.textContent = selected[0];
|
||||
if (clearButton) clearButton.style.display = "block";
|
||||
} else {
|
||||
headerSpan.textContent = `${selected.length} selected`;
|
||||
if (clearButton) clearButton.style.display = "block";
|
||||
}
|
||||
|
||||
// Update URL params
|
||||
const rootUrl = new URL(window.location);
|
||||
if (selected.length > 0) {
|
||||
rootUrl.searchParams.set(param, selected.join(","));
|
||||
} else {
|
||||
rootUrl.searchParams.delete(param);
|
||||
}
|
||||
window.history.pushState({}, "", rootUrl);
|
||||
|
||||
// update content-frame url for Turbo
|
||||
const contentUrl = new URL(window.location);
|
||||
contentUrl.pathname = <%== filterable_dashboard_content_static_pages_path.to_json %>;
|
||||
contentUrl.searchParams.set(param, selected.join(","));
|
||||
frame.src = contentUrl.toString();
|
||||
|
||||
const requestTimestamp = Date.now();
|
||||
window.lastRequestTimestamp = requestTimestamp;
|
||||
|
||||
fetch(contentUrl.toString(), {
|
||||
headers: { Accept: "text/html" },
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((html) => {
|
||||
if (requestTimestamp === window.lastRequestTimestamp) {
|
||||
frame.innerHTML = html;
|
||||
frame.classList.remove("loading");
|
||||
window.hackatimeCharts?.initializeCharts();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.initializeIntervalSelect = window.initializeIntervalSelect || function () {
|
||||
const s = document.getElementById("interval-select");
|
||||
if (!s || s.dataset.initialized) return;
|
||||
s.dataset.initialized = "true";
|
||||
|
||||
const h = document.getElementById("interval-header");
|
||||
const c = document.getElementById("interval-options");
|
||||
const clr = document.getElementById("interval-clear-button");
|
||||
const radios = s.querySelectorAll('input[type="radio"]');
|
||||
const start = document.getElementById("interval-custom-start");
|
||||
const end = document.getElementById("interval-custom-end");
|
||||
|
||||
h.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
document.querySelectorAll(".options-container").forEach((x) => { if (x !== c) x.style.display = "none"; });
|
||||
c.style.display = c.style.display === "block" ? "none" : "block";
|
||||
});
|
||||
|
||||
clr?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
radios.forEach((r) => (r.checked = false));
|
||||
s.querySelector('input[value=""]')?.click();
|
||||
start.value = end.value = "";
|
||||
updateIntervalFilter("", "", "");
|
||||
});
|
||||
|
||||
const hSpan = h.querySelector("span") || h;
|
||||
radios.forEach((r) => r.addEventListener("change", function () {
|
||||
hSpan.textContent = this.closest(".option").dataset.intervalLabel;
|
||||
clr.classList.toggle("hidden", this.value === "");
|
||||
c.style.display = "none";
|
||||
start.value = end.value = "";
|
||||
updateIntervalFilter(this.value, "", "");
|
||||
}));
|
||||
|
||||
document.getElementById("interval-apply-custom").addEventListener("click", () => {
|
||||
const sv = start.value, ev = end.value;
|
||||
if (!sv && !ev) return;
|
||||
hSpan.textContent = sv && ev ? `${sv} to ${ev}` : sv ? `From ${sv}` : `Until ${ev}`;
|
||||
clr.classList.remove("hidden");
|
||||
c.style.display = "none";
|
||||
radios.forEach((r) => (r.checked = false));
|
||||
updateIntervalFilter("custom", sv, ev);
|
||||
});
|
||||
|
||||
[start, end].forEach((i) => i.addEventListener("click", (e) => e.stopPropagation()));
|
||||
};
|
||||
|
||||
window.updateIntervalFilter = window.updateIntervalFilter || function (interval, from, to) {
|
||||
const f = document.querySelector("#filterable_dashboard_content");
|
||||
f.classList.add("loading");
|
||||
|
||||
const url = new URL(window.location);
|
||||
["interval", "from", "to"].forEach((k) => url.searchParams.delete(k));
|
||||
if (interval) url.searchParams.set("interval", interval);
|
||||
if (from) url.searchParams.set("from", from);
|
||||
if (to) url.searchParams.set("to", to);
|
||||
window.history.pushState({}, "", url);
|
||||
|
||||
url.pathname = <%== filterable_dashboard_content_static_pages_path.to_json %>;
|
||||
fetch(url.toString(), { headers: { Accept: "text/html" } })
|
||||
.then((r) => r.text())
|
||||
.then((html) => {
|
||||
f.innerHTML = html;
|
||||
f.classList.remove("loading");
|
||||
window.hackatimeCharts?.initializeCharts();
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener("turbo:frame-load", function (event) {
|
||||
if (event.target.id === "filterable_dashboard") {
|
||||
["project", "language", "editor", "operating_system", "category"].forEach((type) => {
|
||||
window.initializeMultiSelect(`${type}-select`);
|
||||
});
|
||||
window.initializeIntervalSelect();
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!e.target.closest(".custom-select")) {
|
||||
document.querySelectorAll(".options-container").forEach((container) => {
|
||||
container.style.display = "none";
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" data-turbo-track="reload"></script>
|
||||
<script>
|
||||
window.chartInstances = window.chartInstances || {};
|
||||
|
||||
if (!window.hackatimeCharts) {
|
||||
window.hackatimeCharts = {
|
||||
formatDuration(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
},
|
||||
|
||||
createPieChart(elementId) {
|
||||
const canvas = document.getElementById(elementId);
|
||||
if (!canvas) return;
|
||||
const stats = JSON.parse(canvas.dataset.stats);
|
||||
const labels = Object.keys(stats);
|
||||
const data = Object.values(stats);
|
||||
if (window.chartInstances[elementId]) {
|
||||
window.chartInstances[elementId].destroy();
|
||||
}
|
||||
const ctx = canvas.getContext("2d");
|
||||
const pieColors = ["#60a5fa", "#f472b6", "#fb923c", "#facc15", "#4ade80", "#2dd4bf", "#a78bfa", "#f87171", "#38bdf8", "#e879f9", "#34d399", "#fbbf24", "#818cf8", "#fb7185", "#22d3ee", "#a3e635", "#c084fc", "#f97316", "#14b8a6", "#8b5cf6", "#ec4899", "#84cc16", "#06b6d4", "#d946ef", "#10b981"];
|
||||
const backgroundColors = labels.map((_, i) => pieColors[i % pieColors.length]);
|
||||
window.chartInstances[elementId] = new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: { labels, datasets: [{ data, backgroundColor: backgroundColors, borderWidth: 1 }] },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
aspectRatio: 1.2,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
const label = context.label || "";
|
||||
const value = context.raw || 0;
|
||||
const duration = window.hackatimeCharts.formatDuration(value);
|
||||
const percentage = ((value / data.reduce((a, b) => a + b, 0)) * 100).toFixed(1);
|
||||
return `${label}: ${duration} (${percentage}%)`;
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
position: "right",
|
||||
align: "center",
|
||||
labels: {
|
||||
boxWidth: 10,
|
||||
padding: 8,
|
||||
font: { size: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
createProjectTimelineChart() {
|
||||
const canvas = document.getElementById("projectTimelineChart");
|
||||
if (!canvas) return;
|
||||
|
||||
const weeklyStats = JSON.parse(canvas.dataset.stats);
|
||||
const allProjects = new Set();
|
||||
Object.values(weeklyStats).forEach((weekData) => {
|
||||
Object.keys(weekData).forEach((project) => allProjects.add(project));
|
||||
});
|
||||
const sortedWeeks = Object.keys(weeklyStats).sort();
|
||||
const datasets = Array.from(allProjects).map((project) => ({
|
||||
label: project,
|
||||
data: sortedWeeks.map((week) => weeklyStats[week][project] || 0),
|
||||
stack: "stack0",
|
||||
}));
|
||||
|
||||
datasets.sort((a, b) => {
|
||||
const sumA = a.data.reduce((acc, val) => acc + val, 0);
|
||||
const sumB = b.data.reduce((acc, val) => acc + val, 0);
|
||||
return sumB - sumA;
|
||||
});
|
||||
|
||||
if (window.chartInstances["projectTimelineChart"]) {
|
||||
window.chartInstances["projectTimelineChart"].destroy();
|
||||
}
|
||||
const ctx = canvas.getContext("2d");
|
||||
window.chartInstances["projectTimelineChart"] = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: sortedWeeks.map((week) => {
|
||||
const date = new Date(week);
|
||||
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
||||
}),
|
||||
datasets: datasets,
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: { display: false },
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
type: "linear",
|
||||
grid: {
|
||||
color: (ctx) => {
|
||||
if (ctx.tick.value === 0) return "transparent";
|
||||
return ctx.tick.value % 1 === 0 ? "rgba(0, 0, 0, 0.1)" : "rgba(0, 0, 0, 0.05)";
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
if (value === 0) return "0s";
|
||||
const hours = Math.floor(value / 3600);
|
||||
const minutes = Math.floor((value % 3600) / 60);
|
||||
if (hours > 0) return `${hours}h`;
|
||||
return `${minutes}m`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "right",
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
padding: 15,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
const value = context.raw;
|
||||
const hours = Math.floor(value / 3600);
|
||||
const minutes = Math.floor((value % 3600) / 60);
|
||||
if (hours > 0) {
|
||||
return `${context.dataset.label}: ${hours}h ${minutes}m`;
|
||||
}
|
||||
return `${context.dataset.label}: ${minutes}m`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
initializeCharts() {
|
||||
this.createPieChart("languageChart");
|
||||
this.createPieChart("editorChart");
|
||||
this.createPieChart("operatingSystemChart");
|
||||
this.createProjectTimelineChart();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!window.chartListenersInitialized) {
|
||||
window.chartListenersInitialized = true;
|
||||
document.addEventListener("turbo:frame-load", () => {
|
||||
if (typeof Chart === "undefined") {
|
||||
const checkChart = setInterval(() => {
|
||||
if (typeof Chart !== "undefined") {
|
||||
clearInterval(checkChart);
|
||||
window.hackatimeCharts.initializeCharts();
|
||||
}
|
||||
}, 50);
|
||||
setTimeout(() => clearInterval(checkChart), 5000);
|
||||
} else {
|
||||
window.hackatimeCharts.initializeCharts();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (typeof Chart !== "undefined") {
|
||||
window.hackatimeCharts.initializeCharts();
|
||||
}
|
||||
</script>
|
||||
<% end %>
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
<div class="flex flex-col gap-6 w-full">
|
||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(9.375rem,1fr))] gap-4">
|
||||
<div class="bg-dark border border-primary rounded-xl p-4 transition-all duration-200 h-full">
|
||||
<div class="text-secondary text-xs mb-1 uppercase tracking-tight">TOTAL TIME</div>
|
||||
<div class="text-lg font-semibold text-surface-content" data-stat="total_time"><%= ApplicationController.helpers.short_time_simple(@total_time) %></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark border border-primary rounded-xl p-4 transition-all duration-200 h-full">
|
||||
<div class="text-secondary text-xs mb-1 uppercase tracking-tight">TOP PROJECT</div>
|
||||
<div class="text-lg font-semibold text-surface-content" data-stat="top_project">
|
||||
<%= @top_project || "None" %>
|
||||
<% if @singular_project %>
|
||||
<span class="super"><%= FlavorText.obvious.sample %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark border border-primary rounded-xl p-4 transition-all duration-200 h-full">
|
||||
<div class="text-secondary text-xs mb-1 uppercase tracking-tight">TOP LANGUAGE</div>
|
||||
<div class="text-lg font-semibold text-surface-content" data-stat="top_language">
|
||||
<%= display_language_name(@top_language) || "Unknown" %>
|
||||
<% if @singular_language %>
|
||||
<span class="super"><%= FlavorText.obvious.sample %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark border border-primary rounded-xl p-4 transition-all duration-200 h-full">
|
||||
<div class="text-secondary text-xs mb-1 uppercase tracking-tight">TOP OS</div>
|
||||
<div class="text-lg font-semibold text-surface-content" data-stat="top_operating_system">
|
||||
<%= display_os_name(@top_operating_system) || "Unknown" %>
|
||||
<% if @singular_operating_system %>
|
||||
<span class="super"><%= FlavorText.obvious.sample %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark border border-primary rounded-xl p-4 transition-all duration-200 h-full">
|
||||
<div class="text-secondary text-xs mb-1 uppercase tracking-tight">TOP EDITOR</div>
|
||||
<div class="text-lg font-semibold text-surface-content" data-stat="top_editor">
|
||||
<%= display_editor_name(@top_editor) || "Unknown" %>
|
||||
<% if @singular_editor %>
|
||||
<span class="super"><%= FlavorText.obvious.sample %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark border border-primary rounded-xl p-4 transition-all duration-200 h-full">
|
||||
<div class="text-secondary text-xs mb-1 uppercase tracking-tight">TOP CATEGORY</div>
|
||||
<div class="text-lg font-semibold text-surface-content" data-stat="top_category">
|
||||
<%= @top_category&.capitalize || "Unknown" %>
|
||||
<% if @singular_category %>
|
||||
<span class="super"><%= FlavorText.obvious.sample %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 w-full">
|
||||
<% if @project_durations&.size&.> 1 %>
|
||||
<div class="bg-dark border border-primary rounded-xl p-6 flex flex-col">
|
||||
<h2 class="mb-4 text-xl font-semibold text-surface-content">Project Durations</h2>
|
||||
<div class="mt-2">
|
||||
<%
|
||||
max_duration = @project_durations.values.max
|
||||
|
||||
# Use logarithmic scale for better visibility of smaller values
|
||||
# Add 1 to avoid log(0), scale to 15-100 range
|
||||
def log_scale(value, max_val)
|
||||
return 0 if value == 0
|
||||
min_percent = 5 # Minimum bar width percentage
|
||||
max_percent = 100 # Maximum bar width percentage
|
||||
|
||||
# Mix linear and logarithmic scaling
|
||||
# 80% linear, 20% logarithmic
|
||||
linear_ratio = value.to_f / max_val
|
||||
log_ratio = Math.log(value + 1) / Math.log(max_val + 1)
|
||||
|
||||
linear_weight = 0.8
|
||||
log_weight = 0.2
|
||||
|
||||
scaled = min_percent + (linear_weight * linear_ratio + log_weight * log_ratio) * (max_percent - min_percent)
|
||||
[scaled, max_percent].min.round
|
||||
end
|
||||
%>
|
||||
|
||||
<% @project_durations.each do |project, duration| %>
|
||||
<div class="flex items-center mb-3">
|
||||
<div class="w-37.5 text-right pr-4 text-sm text-muted whitespace-nowrap overflow-hidden text-ellipsis"><%= h(project.presence || 'Unknown') %></div>
|
||||
<div class="flex-1 h-7 bg-darkless rounded-lg overflow-hidden">
|
||||
<div class="h-full bg-primary rounded-lg relative transition-[width] duration-300 ease-in-out" style="width: <%= log_scale(duration, max_duration) %>%">
|
||||
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-xs font-medium text-on-primary"><%= ApplicationController.helpers.short_time_simple(duration) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# Language distribution %>
|
||||
<% if @language_stats.present? %>
|
||||
<div class="bg-dark border border-primary rounded-xl p-6 flex flex-col">
|
||||
<h2 class="mb-4 text-xl font-semibold text-surface-content">Languages</h2>
|
||||
<div class="flex-1 relative min-h-48">
|
||||
<canvas id="languageChart" class="max-h-full w-full" data-stats="<%= @language_stats.to_json %>"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# Editor distribution %>
|
||||
<% if @editor_stats.present? %>
|
||||
<div class="bg-dark border border-primary rounded-xl p-6 flex flex-col">
|
||||
<h2 class="mb-4 text-xl font-semibold text-surface-content">Editors</h2>
|
||||
<div class="flex-1 relative min-h-48">
|
||||
<canvas id="editorChart" class="max-h-full w-full" data-stats="<%= @editor_stats.to_json %>"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# OS distribution %>
|
||||
<% if @operating_system_stats.present? %>
|
||||
<div class="bg-dark border border-primary rounded-xl p-6 flex flex-col">
|
||||
<h2 class="mb-4 text-xl font-semibold text-surface-content">Operating Systems</h2>
|
||||
<div class="flex-1 relative min-h-48">
|
||||
<canvas id="operatingSystemChart" class="max-h-full w-full" data-stats="<%= @operating_system_stats.to_json %>"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="bg-dark border border-primary rounded-xl p-6 flex flex-col">
|
||||
<h2 class="mb-4 text-xl font-semibold text-surface-content">Project Timeline</h2>
|
||||
<div class="flex-1 relative min-h-48">
|
||||
<canvas id="projectTimelineChart" class="max-h-full w-full" data-stats="<%= @weekly_project_stats.to_json %>"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
8
bun.lock
8
bun.lock
|
|
@ -24,6 +24,10 @@
|
|||
"vite": "^7.3.1",
|
||||
"vite-plugin-ruby": "^5.1.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
|
|
@ -489,6 +493,10 @@
|
|||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
|
||||
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ Rails.application.configure do
|
|||
# While tests run files are not watched, reloading is not necessary.
|
||||
config.enable_reloading = false
|
||||
|
||||
# Avoid stale precompiled asset manifests in public/assets during tests.
|
||||
# Propshaft switches to dynamic resolution when this manifest file does not exist.
|
||||
config.assets.manifest_path = Rails.root.join("tmp/assets/.manifest.json")
|
||||
|
||||
# Eager loading loads your entire application. When running a single test locally,
|
||||
# this is usually not necessary, and can slow down your test suite. However, it's
|
||||
# recommended that you enable it in continuous integration systems to ensure eager
|
||||
|
|
|
|||
|
|
@ -86,8 +86,6 @@ Rails.application.routes.draw do
|
|||
get :project_durations
|
||||
get :currently_hacking
|
||||
get :currently_hacking_count
|
||||
get :filterable_dashboard_content
|
||||
get :filterable_dashboard
|
||||
get :streak
|
||||
# get :timeline # Removed: Old route for timeline
|
||||
end
|
||||
|
|
@ -204,8 +202,6 @@ Rails.application.routes.draw do
|
|||
post :claim, on: :collection
|
||||
end
|
||||
|
||||
get "dashboard_stats", to: "dashboard_stats#show"
|
||||
|
||||
namespace :my do
|
||||
get "heartbeats/most_recent", to: "heartbeats#most_recent"
|
||||
get "heartbeats", to: "heartbeats#index"
|
||||
|
|
|
|||
111
package-lock.json
generated
111
package-lock.json
generated
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "app",
|
||||
"name": "hackatime-mahadk",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
@ -24,6 +24,10 @@
|
|||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-ruby": "^5.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
|
|
@ -507,37 +511,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@inertiajs/core": {
|
||||
"version": "2.3.14",
|
||||
"resolved": "file:vendor/inertia/packages/core",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"laravel-precognition": "2.0.0-beta.0",
|
||||
"lodash-es": "^4.17.23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": "^1.13.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"axios": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
"resolved": "vendor/inertia/packages/core",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@inertiajs/svelte": {
|
||||
"version": "2.3.14",
|
||||
"resolved": "file:vendor/inertia/packages/svelte",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inertiajs/core": "file:../core",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"laravel-precognition": "2.0.0-beta.0",
|
||||
"lodash-es": "^4.17.23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
}
|
||||
"resolved": "vendor/inertia/packages/svelte",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
|
|
@ -1116,6 +1095,7 @@
|
|||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||
"version": "6.2.4",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
|
|
@ -1443,6 +1423,7 @@
|
|||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -1505,6 +1486,7 @@
|
|||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
|
|
@ -2967,6 +2949,7 @@
|
|||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -3024,6 +3007,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -3172,6 +3156,34 @@
|
|||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-svelte": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.1.tgz",
|
||||
"integrity": "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.0.0",
|
||||
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
|
|
@ -3396,6 +3408,7 @@
|
|||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.51.2.tgz",
|
||||
"integrity": "sha512-AqApqNOxVS97V4Ko9UHTHeSuDJrwauJhZpLDs1gYD8Jk48ntCSWD7NxKje+fnGn5Ja1O3u2FzQZHPdifQjXe3w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
|
|
@ -3453,7 +3466,8 @@
|
|||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.18",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
|
|
@ -3526,6 +3540,7 @@
|
|||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -3541,6 +3556,7 @@
|
|||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -3652,6 +3668,39 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"vendor/inertia/packages/core": {
|
||||
"name": "@inertiajs/core",
|
||||
"version": "2.3.14",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"laravel-precognition": "2.0.0-beta.0",
|
||||
"lodash-es": "^4.17.23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": "^1.13.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"axios": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"vendor/inertia/packages/svelte": {
|
||||
"name": "@inertiajs/svelte",
|
||||
"version": "2.3.14",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inertiajs/core": "file:../core",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"laravel-precognition": "2.0.0-beta.0",
|
||||
"lodash-es": "^4.17.23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"
|
||||
"check": "npm run check:svelte && tsc -p tsconfig.node.json",
|
||||
"check:svelte": "svelte-check --tsconfig ./tsconfig.svelte-check.json",
|
||||
"format:svelte": "prettier --write \"app/javascript/**/*.svelte\"",
|
||||
"format:svelte:check": "prettier --check \"app/javascript/**/*.svelte\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
|
|
@ -24,5 +27,9 @@
|
|||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-ruby": "^5.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
tsconfig.svelte-check.json
Normal file
7
tsconfig.svelte-check.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"checkJs": false
|
||||
},
|
||||
"include": ["app/javascript/**/*.svelte"]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue