mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 19:55:16 +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
|
- name: Lint code for consistent style
|
||||||
run: bin/rubocop -f github
|
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:
|
zeitwerk:
|
||||||
runs-on: ubuntu-latest
|
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}";
|
@source "../../../node_modules/layerchart/dist/**/*.{svelte,js}";
|
||||||
@import "./main.css";
|
@import "./main.css";
|
||||||
@import "./nav.css";
|
@import "./nav.css";
|
||||||
@import "./filterable_dashboard.css";
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply flex min-h-screen;
|
@apply flex min-h-screen;
|
||||||
|
|
@ -291,4 +290,3 @@ html[data-theme="nord"] {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
opacity: 0.9;
|
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
|
include DashboardData
|
||||||
|
|
||||||
layout "inertia", only: :index
|
layout "inertia", only: :index
|
||||||
before_action :ensure_current_user, only: %i[
|
|
||||||
filterable_dashboard
|
|
||||||
filterable_dashboard_content
|
|
||||||
]
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
if current_user
|
if current_user
|
||||||
|
|
@ -108,32 +104,8 @@ class StaticPagesController < InertiaController
|
||||||
render partial: "streak"
|
render partial: "streak"
|
||||||
end
|
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
|
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
|
def set_homepage_seo_content
|
||||||
@page_title = @og_title = @twitter_title = "Hackatime - See How Much You Code"
|
@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."
|
@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_uid_blank: current_user&.github_uid.blank?,
|
||||||
github_auth_path: github_auth_path,
|
github_auth_path: github_auth_path,
|
||||||
wakatime_setup_path: my_wakatime_setup_path,
|
wakatime_setup_path: my_wakatime_setup_path,
|
||||||
dashboard_stats_url: api_v1_dashboard_stats_path(
|
dashboard_stats: InertiaRails.defer { dashboard_stats_payload }
|
||||||
interval: params[:interval],
|
}
|
||||||
from: params[:from],
|
end
|
||||||
to: params[:to],
|
|
||||||
project: params[:project],
|
def dashboard_stats_payload
|
||||||
language: params[:language],
|
{
|
||||||
editor: params[:editor],
|
filterable_dashboard_data: filterable_dashboard_data,
|
||||||
operating_system: params[:operating_system],
|
activity_graph: activity_graph_data,
|
||||||
category: params[:category]
|
today_stats: today_stats_data
|
||||||
)
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Link } from "@inertiajs/svelte";
|
import { Link } from "@inertiajs/svelte";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
type ButtonType = "button" | "submit" | "reset";
|
type ButtonType = "button" | "submit" | "reset";
|
||||||
type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl";
|
type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl";
|
||||||
type ButtonVariant =
|
type ButtonVariant = "primary" | "surface" | "dark" | "outlinePrimary";
|
||||||
| "primary"
|
|
||||||
| "surface"
|
|
||||||
| "dark"
|
|
||||||
| "outlinePrimary";
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
href = "",
|
href = "",
|
||||||
|
|
@ -15,6 +12,7 @@
|
||||||
size = "md",
|
size = "md",
|
||||||
variant = "primary",
|
variant = "primary",
|
||||||
unstyled = false,
|
unstyled = false,
|
||||||
|
children,
|
||||||
class: className = "",
|
class: className = "",
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -23,6 +21,7 @@
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
unstyled?: boolean;
|
unstyled?: boolean;
|
||||||
|
children?: Snippet;
|
||||||
class?: string;
|
class?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
@ -59,11 +58,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if href}
|
{#if href}
|
||||||
<Link href={href} class={classes} {...rest}>
|
<Link {href} class={classes} {...rest}>
|
||||||
<slot />
|
{@render children?.()}
|
||||||
</Link>
|
</Link>
|
||||||
{:else}
|
{:else}
|
||||||
<button type={type} class={classes} {...rest}>
|
<button {type} class={classes} {...rest}>
|
||||||
<slot />
|
{@render children?.()}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { Link, usePoll } from "@inertiajs/svelte";
|
import { Link, usePoll } from "@inertiajs/svelte";
|
||||||
import Button from "../components/Button.svelte";
|
import Button from "../components/Button.svelte";
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy, untrack } from "svelte";
|
||||||
import plur from "plur";
|
import plur from "plur";
|
||||||
|
|
||||||
type NavLink = {
|
type NavLink = {
|
||||||
|
|
@ -88,17 +88,20 @@
|
||||||
let navOpen = $state(false);
|
let navOpen = $state(false);
|
||||||
let logoutOpen = $state(false);
|
let logoutOpen = $state(false);
|
||||||
let currentlyExpanded = $state(false);
|
let currentlyExpanded = $state(false);
|
||||||
let flashVisible = $state(layout.nav.flash.length > 0);
|
let flashVisible = $state(false);
|
||||||
let flashHiding = $state(false);
|
let flashHiding = $state(false);
|
||||||
const flashHideDelay = 6000;
|
const flashHideDelay = 6000;
|
||||||
const flashExitDuration = 250;
|
const flashExitDuration = 250;
|
||||||
|
const pollInterval = untrack(
|
||||||
|
() => layout.currently_hacking?.interval || 30000,
|
||||||
|
);
|
||||||
|
|
||||||
const toggleNav = () => (navOpen = !navOpen);
|
const toggleNav = () => (navOpen = !navOpen);
|
||||||
const closeNav = () => (navOpen = false);
|
const closeNav = () => (navOpen = false);
|
||||||
const openLogout = () => (logoutOpen = true);
|
const openLogout = () => (logoutOpen = true);
|
||||||
const closeLogout = () => (logoutOpen = false);
|
const closeLogout = () => (logoutOpen = false);
|
||||||
|
|
||||||
usePoll(layout.currently_hacking?.interval || 30000, {
|
usePoll(pollInterval, {
|
||||||
only: ["currently_hacking"],
|
only: ["currently_hacking"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -117,6 +120,13 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleModalBackdropKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
closeLogout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const countLabel = () =>
|
const countLabel = () =>
|
||||||
`${layout.currently_hacking.count} ${plur("person", layout.currently_hacking.count)} currently hacking`;
|
`${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) => {
|
const adminLevelLabel = (adminLevel?: AdminLevel | null) => {
|
||||||
if (adminLevel === "superadmin") return "Superadmin";
|
if (adminLevel === "superadmin") return "Superadmin";
|
||||||
|
|
@ -218,9 +229,7 @@
|
||||||
|
|
||||||
document.documentElement.setAttribute("data-theme", layout.theme.name);
|
document.documentElement.setAttribute("data-theme", layout.theme.name);
|
||||||
|
|
||||||
const colorSchemeMeta = document.querySelector(
|
const colorSchemeMeta = document.querySelector("meta[name='color-scheme']");
|
||||||
"meta[name='color-scheme']",
|
|
||||||
);
|
|
||||||
colorSchemeMeta?.setAttribute("content", layout.theme.color_scheme);
|
colorSchemeMeta?.setAttribute("content", layout.theme.color_scheme);
|
||||||
|
|
||||||
const themeColorMeta = document.querySelector("meta[name='theme-color']");
|
const themeColorMeta = document.querySelector("meta[name='theme-color']");
|
||||||
|
|
@ -312,7 +321,13 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</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
|
<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"
|
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"
|
class="flex flex-col items-center gap-2 pb-3 border-b border-darkless"
|
||||||
>
|
>
|
||||||
{#if layout.nav.current_user}
|
{#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}
|
{#if layout.nav.current_user.avatar_url}
|
||||||
<img
|
<img
|
||||||
src={layout.nav.current_user.avatar_url}
|
src={layout.nav.current_user.avatar_url}
|
||||||
|
|
@ -343,7 +361,8 @@
|
||||||
{#if layout.nav.current_user.country_code}
|
{#if layout.nav.current_user.country_code}
|
||||||
<span
|
<span
|
||||||
class="flex items-center"
|
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)}
|
{countryFlagEmoji(layout.nav.current_user.country_code)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -351,10 +370,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if layout.nav.current_user.streak_days && layout.nav.current_user.streak_days > 0}
|
{#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
|
<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`}
|
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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -369,9 +392,13 @@
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</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)}
|
{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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -401,22 +428,21 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={openLogout}
|
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}
|
{:else}
|
||||||
{#if link.inertia}
|
<a
|
||||||
<Link
|
href={link.href || "#"}
|
||||||
href={link.href || "#"}
|
onclick={handleNavLinkClick}
|
||||||
onclick={handleNavLinkClick}
|
class={navLinkClass(link.active)}>{link.label}</a
|
||||||
class={navLinkClass(link.active)}>{link.label}</Link
|
>
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<a
|
|
||||||
href={link.href || "#"}
|
|
||||||
onclick={handleNavLinkClick}
|
|
||||||
class={navLinkClass(link.active)}>{link.label}</a
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
|
@ -584,8 +610,8 @@
|
||||||
>
|
>
|
||||||
from {layout.footer.server_start_time_ago} ago.
|
from {layout.footer.server_start_time_ago} ago.
|
||||||
{plur("heartbeat", layout.footer.heartbeat_recent_count)}
|
{plur("heartbeat", layout.footer.heartbeat_recent_count)}
|
||||||
({layout.footer.heartbeat_recent_imported_count} imported) in the past
|
({layout.footer.heartbeat_recent_imported_count} imported) in the past 24
|
||||||
24 hours. (DB: {layout.footer.query_count}
|
hours. (DB: {layout.footer.query_count}
|
||||||
{plur("query", layout.footer.query_count)}, {layout.footer
|
{plur("query", layout.footer.query_count)}, {layout.footer
|
||||||
.query_cache_count} cached) (CACHE: {layout.footer.cache_hits} hits,
|
.query_cache_count} cached) (CACHE: {layout.footer.cache_hits} hits,
|
||||||
{layout.footer.cache_misses} misses) ({layout.footer
|
{layout.footer.cache_misses} misses) ({layout.footer
|
||||||
|
|
@ -617,19 +643,21 @@
|
||||||
<div
|
<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"
|
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"
|
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}
|
onclick={toggleCurrentlyHacking}
|
||||||
>
|
>
|
||||||
<div class="text-surface-content text-sm font-medium">
|
<div class="text-surface-content text-sm font-medium">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div
|
<div class="w-2 h-2 rounded-full bg-green animate-pulse mr-2"></div>
|
||||||
class="w-2 h-2 rounded-full bg-green animate-pulse mr-2"
|
|
||||||
></div>
|
|
||||||
<span class="text-base">{countLabel()}</span>
|
<span class="text-base">{countLabel()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
|
|
||||||
{#if currentlyExpanded}
|
{#if currentlyExpanded}
|
||||||
{#if layout.currently_hacking.users.length === 0}
|
{#if layout.currently_hacking.users.length === 0}
|
||||||
|
|
@ -707,7 +735,11 @@
|
||||||
class:opacity-0={!logoutOpen}
|
class:opacity-0={!logoutOpen}
|
||||||
class:pointer-events-none={!logoutOpen}
|
class:pointer-events-none={!logoutOpen}
|
||||||
style="background-color: rgba(0, 0, 0, 0.5);backdrop-filter: blur(4px);"
|
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()}
|
onclick={(e) => e.target === e.currentTarget && closeLogout()}
|
||||||
|
onkeydown={handleModalBackdropKeydown}
|
||||||
>
|
>
|
||||||
<div
|
<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"}`}
|
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>
|
</svg>
|
||||||
</div>
|
</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
|
Woah hold on a sec
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-muted mb-6 text-center w-full">
|
<p class="text-muted mb-6 text-center w-full">
|
||||||
|
|
@ -741,8 +775,7 @@
|
||||||
type="button"
|
type="button"
|
||||||
onclick={closeLogout}
|
onclick={closeLogout}
|
||||||
variant="dark"
|
variant="dark"
|
||||||
class="w-full h-10 text-muted m-0"
|
class="w-full h-10 text-muted m-0">Go back</Button
|
||||||
>Go back</Button
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
|
|
@ -753,10 +786,7 @@
|
||||||
value={layout.csrf_token}
|
value={layout.csrf_token}
|
||||||
/>
|
/>
|
||||||
<input type="hidden" name="_method" value="delete" />
|
<input type="hidden" name="_method" value="delete" />
|
||||||
<Button
|
<Button type="submit" variant="primary" class="w-full h-10 m-0"
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
class="w-full h-10 m-0"
|
|
||||||
>Log out now</Button
|
>Log out now</Button
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,16 @@
|
||||||
href="/my/wakatime_setup"
|
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"
|
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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<h3 class="font-semibold text-surface-content">Quick Start</h3>
|
<h3 class="font-semibold text-surface-content">Quick Start</h3>
|
||||||
<p class="text-sm text-muted text-center mt-1">
|
<p class="text-sm text-muted text-center mt-1">
|
||||||
|
|
@ -44,8 +52,16 @@
|
||||||
href="/docs/getting-started/installation"
|
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"
|
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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<h3 class="font-semibold text-surface-content">Installation</h3>
|
<h3 class="font-semibold text-surface-content">Installation</h3>
|
||||||
<p class="text-sm text-muted text-center mt-1">Add to your editor</p>
|
<p class="text-sm text-muted text-center mt-1">Add to your editor</p>
|
||||||
|
|
@ -55,8 +71,16 @@
|
||||||
href="/api-docs"
|
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"
|
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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<h3 class="font-semibold text-surface-content">API Docs</h3>
|
<h3 class="font-semibold text-surface-content">API Docs</h3>
|
||||||
<p class="text-sm text-muted text-center mt-1">Interactive reference</p>
|
<p class="text-sm text-muted text-center mt-1">Interactive reference</p>
|
||||||
|
|
@ -66,8 +90,16 @@
|
||||||
href="/docs/oauth/oauth-apps"
|
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"
|
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">
|
<svg
|
||||||
<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" />
|
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>
|
</svg>
|
||||||
<h3 class="font-semibold text-surface-content">OAuth Apps</h3>
|
<h3 class="font-semibold text-surface-content">OAuth Apps</h3>
|
||||||
<p class="text-sm text-muted text-center mt-1">Build integrations</p>
|
<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>
|
<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>
|
<h2 class="text-2xl font-semibold text-surface-content mb-4">{title}</h2>
|
||||||
<p class="text-secondary mb-8">{message}</p>
|
<p class="text-secondary mb-8">{message}</p>
|
||||||
<Button
|
<Button href="/" size="lg" class="hover:brightness-110 transition-all">
|
||||||
href="/"
|
|
||||||
size="lg"
|
|
||||||
class="hover:brightness-110 transition-all"
|
|
||||||
>
|
|
||||||
Go Home
|
Go Home
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { Deferred, router } from "@inertiajs/svelte";
|
||||||
import type { ActivityGraphData } from "../../types/index";
|
import type { ActivityGraphData } from "../../types/index";
|
||||||
import BanNotice from "./signedIn/BanNotice.svelte";
|
import BanNotice from "./signedIn/BanNotice.svelte";
|
||||||
import GitHubLinkBanner from "./signedIn/GitHubLinkBanner.svelte";
|
import GitHubLinkBanner from "./signedIn/GitHubLinkBanner.svelte";
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
github_uid_blank,
|
github_uid_blank,
|
||||||
github_auth_path,
|
github_auth_path,
|
||||||
wakatime_setup_path,
|
wakatime_setup_path,
|
||||||
dashboard_stats_url,
|
dashboard_stats,
|
||||||
}: {
|
}: {
|
||||||
flavor_text: string;
|
flavor_text: string;
|
||||||
trust_level_red: boolean;
|
trust_level_red: boolean;
|
||||||
|
|
@ -69,53 +69,22 @@
|
||||||
github_uid_blank: boolean;
|
github_uid_blank: boolean;
|
||||||
github_auth_path: string;
|
github_auth_path: string;
|
||||||
wakatime_setup_path: string;
|
wakatime_setup_path: string;
|
||||||
dashboard_stats_url: string;
|
dashboard_stats?: {
|
||||||
|
filterable_dashboard_data: FilterableDashboardData;
|
||||||
|
activity_graph: ActivityGraphData;
|
||||||
|
today_stats: TodayStats;
|
||||||
|
};
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let loading = $state(true);
|
function refreshDashboardData(search: string) {
|
||||||
let todayStats = $state<TodayStats | null>(null);
|
router.visit(`${window.location.pathname}${search}`, {
|
||||||
let dashboardData = $state<FilterableDashboardData | null>(null);
|
only: ["dashboard_stats"],
|
||||||
let activityGraph = $state<ActivityGraphData | null>(null);
|
preserveState: true,
|
||||||
let requestSequence = 0;
|
preserveScroll: true,
|
||||||
|
replace: true,
|
||||||
function buildDashboardStatsUrl(search: string) {
|
async: true,
|
||||||
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}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -149,33 +118,46 @@
|
||||||
<GitHubLinkBanner {github_auth_path} />
|
<GitHubLinkBanner {github_auth_path} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex flex-col gap-8">
|
<Deferred data="dashboard_stats">
|
||||||
<!-- Today Stats -->
|
{#snippet fallback()}
|
||||||
<div>
|
<div class="flex flex-col gap-8">
|
||||||
{#if loading}
|
<div>
|
||||||
<TodaySentenceSkeleton />
|
<TodaySentenceSkeleton />
|
||||||
{:else if todayStats}
|
</div>
|
||||||
<TodaySentence
|
<DashboardSkeleton />
|
||||||
show_logged_time_sentence={todayStats.show_logged_time_sentence}
|
<ActivityGraphSkeleton />
|
||||||
todays_duration_display={todayStats.todays_duration_display}
|
</div>
|
||||||
todays_languages={todayStats.todays_languages}
|
{/snippet}
|
||||||
todays_editors={todayStats.todays_editors}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Dashboard -->
|
{#snippet children({ reloading })}
|
||||||
{#if loading}
|
<div class="flex flex-col gap-8" class:opacity-60={reloading}>
|
||||||
<DashboardSkeleton />
|
<!-- Today Stats -->
|
||||||
{:else if dashboardData}
|
<div>
|
||||||
<Dashboard data={dashboardData} onFiltersChange={refreshDashboardData} />
|
{#if dashboard_stats?.today_stats}
|
||||||
{/if}
|
<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 -->
|
<!-- Main Dashboard -->
|
||||||
{#if loading}
|
{#if dashboard_stats?.filterable_dashboard_data}
|
||||||
<ActivityGraphSkeleton />
|
<Dashboard
|
||||||
{:else if activityGraph}
|
data={dashboard_stats.filterable_dashboard_data}
|
||||||
<ActivityGraph data={activityGraph} />
|
onFiltersChange={refreshDashboardData}
|
||||||
{/if}
|
/>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
|
<!-- Activity Graph -->
|
||||||
|
{#if dashboard_stats?.activity_graph}
|
||||||
|
<ActivityGraph data={dashboard_stats.activity_graph} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Deferred>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,9 @@
|
||||||
<div class="hidden md:flex gap-8 text-sm font-medium text-text-muted">
|
<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="#stats" class="hover:text-white transition-colors">Stats</a>
|
||||||
<a href="#editors" class="hover:text-white transition-colors">Editors</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>
|
||||||
<div class="min-w-[140px] flex justify-end">
|
<div class="min-w-[140px] flex justify-end">
|
||||||
<a
|
<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">
|
<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>
|
<svg
|
||||||
<span class="text-3xl font-bold block ml-2">Hold up! Your account has been banned for suspicious activity.</span>
|
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>
|
||||||
<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">
|
||||||
<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>
|
<b>What does this mean?</b> Your account was convicted for fraud or abuse of
|
||||||
<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>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,9 @@
|
||||||
onFiltersChange,
|
onFiltersChange,
|
||||||
}: {
|
}: {
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
onFiltersChange?: (search: string) => Promise<void> | void;
|
onFiltersChange?: (search: string) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let loading = $state(false);
|
|
||||||
|
|
||||||
const langStats = $derived(
|
const langStats = $derived(
|
||||||
(data.language_stats || {}) as Record<string, number>,
|
(data.language_stats || {}) as Record<string, number>,
|
||||||
);
|
);
|
||||||
|
|
@ -36,7 +34,7 @@
|
||||||
const capitalize = (s: string) =>
|
const capitalize = (s: string) =>
|
||||||
s ? s.charAt(0).toUpperCase() + s.slice(1) : "";
|
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);
|
const current = new URL(window.location.href);
|
||||||
for (const [k, v] of Object.entries(overrides)) {
|
for (const [k, v] of Object.entries(overrides)) {
|
||||||
if (v) {
|
if (v) {
|
||||||
|
|
@ -45,31 +43,23 @@
|
||||||
current.searchParams.delete(k);
|
current.searchParams.delete(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onFiltersChange?.(current.search);
|
||||||
window.history.pushState({}, "", current.pathname + current.search);
|
|
||||||
|
|
||||||
loading = true;
|
|
||||||
try {
|
|
||||||
await onFiltersChange?.(current.search);
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onIntervalChange(interval: string, from: string, to: string) {
|
function onIntervalChange(interval: string, from: string, to: string) {
|
||||||
if (from || to) {
|
if (from || to) {
|
||||||
void applyFilters({ interval: "custom", from, to });
|
applyFilters({ interval: "custom", from, to });
|
||||||
} else {
|
} else {
|
||||||
void applyFilters({ interval, from: "", to: "" });
|
applyFilters({ interval, from: "", to: "" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFilterChange(param: string, selected: string[]) {
|
function onFilterChange(param: string, selected: string[]) {
|
||||||
void applyFilters({ [param]: selected.join(",") });
|
applyFilters({ [param]: selected.join(",") });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-6 w-full" class:opacity-60={loading}>
|
<div class="flex flex-col gap-6 w-full">
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 mb-2">
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 mb-2">
|
||||||
<IntervalSelect
|
<IntervalSelect
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-dark border border-primary/40 rounded-xl p-4 md:p-5 mb-4">
|
<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">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,8 @@
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
document.addEventListener("click", handleClickOutside, true);
|
document.addEventListener("click", handleClickOutside, true);
|
||||||
return () => document.removeEventListener("click", handleClickOutside, true);
|
return () =>
|
||||||
|
document.removeEventListener("click", handleClickOutside, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -82,11 +83,15 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="filter relative" bind:this={container}>
|
<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
|
Date Range
|
||||||
</span>
|
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
unstyled
|
unstyled
|
||||||
|
|
@ -94,8 +99,18 @@
|
||||||
onclick={() => (open = !open)}
|
onclick={() => (open = !open)}
|
||||||
>
|
>
|
||||||
<span>{displayLabel}</span>
|
<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">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
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>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
@ -112,10 +127,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if open}
|
{#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">
|
<div class="overflow-y-auto m-0 max-h-56">
|
||||||
{#each INTERVALS as interval}
|
{#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
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="interval"
|
name="interval"
|
||||||
|
|
|
||||||
|
|
@ -68,18 +68,26 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="filter relative" bind:this={container}>
|
<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}
|
{label}
|
||||||
</span>
|
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
unstyled
|
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"
|
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)}
|
onclick={() => (open = !open)}
|
||||||
>
|
>
|
||||||
<span class="truncate {selected.length === 0 ? 'text-surface-content/60' : ''}">
|
<span
|
||||||
|
class="truncate {selected.length === 0
|
||||||
|
? 'text-surface-content/60'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
{displayText}
|
{displayText}
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -88,7 +96,12 @@
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
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>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
@ -105,7 +118,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if open}
|
{#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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
|
|
@ -115,7 +130,9 @@
|
||||||
|
|
||||||
<div class="overflow-y-auto m-0 max-h-64">
|
<div class="overflow-y-auto m-0 max-h-64">
|
||||||
{#each filtered as value}
|
{#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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selected.includes(value)}
|
checked={selected.includes(value)}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,7 @@
|
||||||
{#if entries.length > 0}
|
{#if entries.length > 0}
|
||||||
<div class="flex flex-col gap-2 max-h-96 overflow-y-auto">
|
<div class="flex flex-col gap-2 max-h-96 overflow-y-auto">
|
||||||
{#each entries as [week, stats]}
|
{#each entries as [week, stats]}
|
||||||
{@const total = Object.values(stats).reduce(
|
{@const total = Object.values(stats).reduce((a, v) => a + (v || 0), 0)}
|
||||||
(a, v) => a + (v || 0),
|
|
||||||
0,
|
|
||||||
)}
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-28 text-sm text-muted">{week}</div>
|
<div class="w-28 text-sm text-muted">{week}</div>
|
||||||
<div class="flex-1 bg-darkless rounded h-3 overflow-hidden">
|
<div class="flex-1 bg-darkless rounded h-3 overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,26 @@
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const PIE_COLORS = [
|
const PIE_COLORS = [
|
||||||
"#60a5fa", "#f472b6", "#fb923c", "#facc15", "#4ade80",
|
"#60a5fa",
|
||||||
"#2dd4bf", "#a78bfa", "#f87171", "#38bdf8", "#e879f9",
|
"#f472b6",
|
||||||
"#34d399", "#fbbf24", "#818cf8", "#fb7185", "#22d3ee",
|
"#fb923c",
|
||||||
"#a3e635", "#c084fc", "#f97316", "#14b8a6", "#8b5cf6",
|
"#facc15",
|
||||||
|
"#4ade80",
|
||||||
|
"#2dd4bf",
|
||||||
|
"#a78bfa",
|
||||||
|
"#f87171",
|
||||||
|
"#38bdf8",
|
||||||
|
"#e879f9",
|
||||||
|
"#34d399",
|
||||||
|
"#fbbf24",
|
||||||
|
"#818cf8",
|
||||||
|
"#fb7185",
|
||||||
|
"#22d3ee",
|
||||||
|
"#a3e635",
|
||||||
|
"#c084fc",
|
||||||
|
"#f97316",
|
||||||
|
"#14b8a6",
|
||||||
|
"#8b5cf6",
|
||||||
];
|
];
|
||||||
|
|
||||||
const sortedWeeks = $derived(Object.keys(weeklyStats).sort());
|
const sortedWeeks = $derived(Object.keys(weeklyStats).sort());
|
||||||
|
|
@ -74,7 +90,10 @@
|
||||||
|
|
||||||
type TimelineDatum = Record<string, string | number>;
|
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];
|
const value = datum?.[key];
|
||||||
return typeof value === "number" ? value : 0;
|
return typeof value === "number" ? value : 0;
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +102,9 @@
|
||||||
<div
|
<div
|
||||||
class="bg-dark/50 border border-surface-200 rounded-xl p-6 flex flex-col min-h-[400px]"
|
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}
|
{#if data.length > 0}
|
||||||
<div class="h-[350px]">
|
<div class="h-[350px]">
|
||||||
<BarChart
|
<BarChart
|
||||||
|
|
@ -108,7 +129,7 @@
|
||||||
{@const value = getSeriesValue(data, s.key)}
|
{@const value = getSeriesValue(data, s.key)}
|
||||||
<Tooltip.Item
|
<Tooltip.Item
|
||||||
label={s.label ?? s.key}
|
label={s.label ?? s.key}
|
||||||
value={value}
|
{value}
|
||||||
color={s.color}
|
color={s.color}
|
||||||
format={formatDuration}
|
format={formatDuration}
|
||||||
valueAlign="right"
|
valueAlign="right"
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,18 @@
|
||||||
|
|
||||||
<div class="text-left mt-2 mb-4 flex flex-col">
|
<div class="text-left mt-2 mb-4 flex flex-col">
|
||||||
<p class="mb-4 text-xl text-primary">
|
<p class="mb-4 text-xl text-primary">
|
||||||
Hello friend! Looks like you are new around here, let's get you set up
|
Hello friend! Looks like you are new around here, let's get you set up so
|
||||||
so you can start tracking your coding time.
|
you can start tracking your coding time.
|
||||||
</p>
|
</p>
|
||||||
<div class="mb-4 rounded-xl border border-primary/40 bg-surface-100/70 p-4 md:p-5">
|
<div
|
||||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
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">
|
<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>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
href={wakatime_setup_path}
|
href={wakatime_setup_path}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@
|
||||||
{value || "—"}
|
{value || "—"}
|
||||||
</div>
|
</div>
|
||||||
{#if subtitle}
|
{#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
|
>{subtitle}</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -89,20 +89,21 @@
|
||||||
>
|
>
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<section>
|
<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">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Use the setup guide if you are configuring a new editor or device.
|
Use the setup guide if you are configuring a new editor or device.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button href={paths.wakatime_setup_path} class="mt-4">
|
||||||
href={paths.wakatime_setup_path}
|
|
||||||
class="mt-4"
|
|
||||||
>
|
|
||||||
Open setup guide
|
Open setup guide
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="user_hackatime_extension">
|
<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">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Choose how coding time appears in the extension status text.
|
Choose how coding time appears in the extension status text.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -111,7 +112,10 @@
|
||||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||||
|
|
||||||
<div>
|
<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
|
Display style
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -126,12 +130,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" variant="primary">Save extension settings</Button>
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Save extension settings
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -150,7 +149,9 @@
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{#if rotatedApiKeyError}
|
{#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}
|
{rotatedApiKeyError}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -160,7 +161,9 @@
|
||||||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">
|
<p class="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||||
New API key
|
New API key
|
||||||
</p>
|
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="surface"
|
variant="surface"
|
||||||
|
|
@ -175,15 +178,22 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="user_config_file">
|
<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">
|
<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>
|
</p>
|
||||||
|
|
||||||
{#if config_file.has_api_key && config_file.content}
|
{#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}
|
{: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}
|
{config_file.empty_message}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@
|
||||||
{#if admin_tools.visible}
|
{#if admin_tools.visible}
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<section id="wakatime_mirror">
|
<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">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Mirror heartbeats to external WakaTime-compatible endpoints.
|
Mirror heartbeats to external WakaTime-compatible endpoints.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -49,7 +51,9 @@
|
||||||
<p class="text-sm font-semibold text-surface-content">
|
<p class="text-sm font-semibold text-surface-content">
|
||||||
{mirror.endpoint_url}
|
{mirror.endpoint_url}
|
||||||
</p>
|
</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
|
<form
|
||||||
method="post"
|
method="post"
|
||||||
action={mirror.destroy_path}
|
action={mirror.destroy_path}
|
||||||
|
|
@ -61,12 +65,12 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="_method" value="delete" />
|
<input type="hidden" name="_method" value="delete" />
|
||||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
<input
|
||||||
<Button
|
type="hidden"
|
||||||
type="submit"
|
name="authenticity_token"
|
||||||
variant="surface"
|
value={csrfToken}
|
||||||
size="xs"
|
/>
|
||||||
>
|
<Button type="submit" variant="surface" size="xs">
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -75,10 +79,17 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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} />
|
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||||
<div>
|
<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
|
Endpoint URL
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -91,7 +102,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
WakaTime API Key
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
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>
|
</div>
|
||||||
<Button
|
<Button type="submit" variant="primary">Add mirror</Button>
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Add mirror
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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.
|
You are not authorized to access this section.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@
|
||||||
let selectedProject = $state("");
|
let selectedProject = $state("");
|
||||||
|
|
||||||
$effect(() => {
|
$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);
|
selectedTheme = defaultTheme(options.badge_themes);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -67,7 +70,10 @@
|
||||||
|
|
||||||
<div class="mt-4 space-y-4">
|
<div class="mt-4 space-y-4">
|
||||||
<div>
|
<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
|
Theme
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -82,14 +88,22 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-md border border-surface-200 bg-darker p-4">
|
<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" />
|
<img
|
||||||
<pre class="mt-3 overflow-x-auto text-xs text-surface-content">{badgeUrl()}</pre>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if badges.projects.length > 0 && badges.project_badge_base_url}
|
{#if badges.projects.length > 0 && badges.project_badge_base_url}
|
||||||
<div class="mt-6 border-t border-surface-200 pt-6">
|
<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
|
Project
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -107,20 +121,24 @@
|
||||||
alt="Project stats badge preview"
|
alt="Project stats badge preview"
|
||||||
class="max-w-full rounded"
|
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>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="user_markscribe">
|
<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">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Use this snippet with markscribe to include your coding stats in a
|
Use this snippet with markscribe to include your coding stats in a
|
||||||
README.
|
README.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-4 rounded-md border border-surface-200 bg-darker p-4">
|
<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>
|
</div>
|
||||||
<p class="mt-3 text-sm text-muted">
|
<p class="mt-3 text-sm text-muted">
|
||||||
Reference:
|
Reference:
|
||||||
|
|
|
||||||
|
|
@ -40,24 +40,23 @@
|
||||||
>
|
>
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<section id="user_migration_assistant">
|
<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">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Queue migration of heartbeats and API keys from legacy Hackatime.
|
Queue migration of heartbeats and API keys from legacy Hackatime.
|
||||||
</p>
|
</p>
|
||||||
<form method="post" action={paths.migrate_heartbeats_path} class="mt-4">
|
<form method="post" action={paths.migrate_heartbeats_path} class="mt-4">
|
||||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||||
<Button
|
<Button type="submit" class="rounded-md">Start migration</Button>
|
||||||
type="submit"
|
|
||||||
class="rounded-md"
|
|
||||||
>
|
|
||||||
Start migration
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{#if migration.jobs.length > 0}
|
{#if migration.jobs.length > 0}
|
||||||
<div class="mt-4 space-y-2">
|
<div class="mt-4 space-y-2">
|
||||||
{#each migration.jobs as job}
|
{#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}
|
Job {job.id}: {job.status}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -69,7 +68,9 @@
|
||||||
<h2 class="text-xl font-semibold text-surface-content">Download Data</h2>
|
<h2 class="text-xl font-semibold text-surface-content">Download Data</h2>
|
||||||
|
|
||||||
{#if data_export.is_restricted}
|
{#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.
|
Data export is currently restricted for this account.
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -79,19 +80,25 @@
|
||||||
|
|
||||||
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-3">
|
<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">
|
<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">
|
<p class="mt-1 text-lg font-semibold text-surface-content">
|
||||||
{data_export.total_heartbeats}
|
{data_export.total_heartbeats}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-md border border-surface-200 bg-darker px-3 py-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 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">
|
<p class="mt-1 text-lg font-semibold text-surface-content">
|
||||||
{data_export.total_coding_time}
|
{data_export.total_coding_time}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-md border border-surface-200 bg-darker px-3 py-3">
|
<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">
|
<p class="mt-1 text-lg font-semibold text-surface-content">
|
||||||
{data_export.heartbeats_last_7_days}
|
{data_export.heartbeats_last_7_days}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -99,10 +106,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 space-y-3">
|
<div class="mt-4 space-y-3">
|
||||||
<Button
|
<Button href={paths.export_all_heartbeats_path} class="rounded-md">
|
||||||
href={paths.export_all_heartbeats_path}
|
|
||||||
class="rounded-md"
|
|
||||||
>
|
|
||||||
Export all heartbeats
|
Export all heartbeats
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
@ -123,11 +127,7 @@
|
||||||
required
|
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"
|
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
|
<Button type="submit" variant="surface" class="rounded-md">
|
||||||
type="submit"
|
|
||||||
variant="surface"
|
|
||||||
class="rounded-md"
|
|
||||||
>
|
|
||||||
Export date range
|
Export date range
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -141,7 +141,10 @@
|
||||||
class="mt-4 rounded-md border border-surface-200 bg-darker p-4"
|
class="mt-4 rounded-md border border-surface-200 bg-darker p-4"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
<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)
|
Import heartbeats (development only)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -152,11 +155,7 @@
|
||||||
required
|
required
|
||||||
class="w-full rounded-md border border-surface-200 bg-surface px-3 py-2 text-sm text-surface-content"
|
class="w-full rounded-md border border-surface-200 bg-surface px-3 py-2 text-sm text-surface-content"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button type="submit" variant="surface" class="mt-3 rounded-md">
|
||||||
type="submit"
|
|
||||||
variant="surface"
|
|
||||||
class="mt-3 rounded-md"
|
|
||||||
>
|
|
||||||
Import file
|
Import file
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -165,11 +164,13 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="delete_account">
|
<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}
|
{#if user.can_request_deletion}
|
||||||
<p class="mt-1 text-sm text-muted">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Request permanent deletion. The account enters a waiting period
|
Request permanent deletion. The account enters a waiting period before
|
||||||
before final removal.
|
final removal.
|
||||||
</p>
|
</p>
|
||||||
<form
|
<form
|
||||||
method="post"
|
method="post"
|
||||||
|
|
@ -186,16 +187,14 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||||
<Button
|
<Button type="submit" variant="surface" class="rounded-md">
|
||||||
type="submit"
|
|
||||||
variant="surface"
|
|
||||||
class="rounded-md"
|
|
||||||
>
|
|
||||||
Request deletion
|
Request deletion
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
{:else}
|
{: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.
|
Deletion request is unavailable for this account right now.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@
|
||||||
>
|
>
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<section id="user_slack_status">
|
<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">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Keep your Slack status updated while you are actively coding.
|
Keep your Slack status updated while you are actively coding.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -76,10 +78,14 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="user_slack_notifications">
|
<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">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Enable notifications in any channel by running
|
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
|
/sailorslog on
|
||||||
</code>
|
</code>
|
||||||
in that channel.
|
in that channel.
|
||||||
|
|
@ -88,26 +94,36 @@
|
||||||
{#if slack.notification_channels.length > 0}
|
{#if slack.notification_channels.length > 0}
|
||||||
<ul class="mt-4 space-y-2">
|
<ul class="mt-4 space-y-2">
|
||||||
{#each slack.notification_channels as channel}
|
{#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">
|
<li
|
||||||
<a href={channel.url} target="_blank" class="underline">{channel.label}</a>
|
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>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{:else}
|
{: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.
|
No channel notifications are enabled.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="user_github_account">
|
<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">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Connect GitHub to show project links in dashboards and leaderboards.
|
Connect GitHub to show project links in dashboards and leaderboards.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if github.connected && github.username}
|
{#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
|
Connected as
|
||||||
<a href={github.profile_url || "#"} target="_blank" class="underline">
|
<a href={github.profile_url || "#"} target="_blank" class="underline">
|
||||||
@{github.username}
|
@{github.username}
|
||||||
|
|
@ -135,11 +151,7 @@
|
||||||
>
|
>
|
||||||
<input type="hidden" name="_method" value="delete" />
|
<input type="hidden" name="_method" value="delete" />
|
||||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||||
<Button
|
<Button type="submit" variant="surface" class="rounded-md">
|
||||||
type="submit"
|
|
||||||
variant="surface"
|
|
||||||
class="rounded-md"
|
|
||||||
>
|
|
||||||
Unlink GitHub
|
Unlink GitHub
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -155,7 +167,9 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="user_email_addresses">
|
<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">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Add or remove email addresses used for sign-in and verification.
|
Add or remove email addresses used for sign-in and verification.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -163,7 +177,9 @@
|
||||||
<div class="mt-4 space-y-2">
|
<div class="mt-4 space-y-2">
|
||||||
{#if emails.length > 0}
|
{#if emails.length > 0}
|
||||||
{#each emails as email}
|
{#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">
|
<div class="grow text-sm text-surface-content">
|
||||||
<p>{email.email}</p>
|
<p>{email.email}</p>
|
||||||
<p class="text-xs text-muted">{email.source}</p>
|
<p class="text-xs text-muted">{email.source}</p>
|
||||||
|
|
@ -171,7 +187,11 @@
|
||||||
{#if email.can_unlink}
|
{#if email.can_unlink}
|
||||||
<form method="post" action={paths.unlink_email_path}>
|
<form method="post" action={paths.unlink_email_path}>
|
||||||
<input type="hidden" name="_method" value="delete" />
|
<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} />
|
<input type="hidden" name="email" value={email.email} />
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
@ -186,13 +206,19 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{: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.
|
No email addresses are linked.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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="hidden" name="authenticity_token" value={csrfToken} />
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -201,12 +227,7 @@
|
||||||
placeholder="name@example.com"
|
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"
|
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
|
<Button type="submit" class="rounded-md">Add email</Button>
|
||||||
type="submit"
|
|
||||||
class="rounded-md"
|
|
||||||
>
|
|
||||||
Add email
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
}: ProfilePageProps = $props();
|
}: ProfilePageProps = $props();
|
||||||
|
|
||||||
let csrfToken = $state("");
|
let csrfToken = $state("");
|
||||||
let selectedTheme = $state(user.theme || "gruvbox_dark");
|
let selectedTheme = $state("gruvbox_dark");
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
csrfToken =
|
csrfToken =
|
||||||
|
|
@ -28,6 +28,10 @@
|
||||||
.querySelector("meta[name='csrf-token']")
|
.querySelector("meta[name='csrf-token']")
|
||||||
?.getAttribute("content") || "";
|
?.getAttribute("content") || "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
selectedTheme = user.theme || "gruvbox_dark";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsShell
|
<SettingsShell
|
||||||
|
|
@ -41,7 +45,9 @@
|
||||||
>
|
>
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<section id="user_region">
|
<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">
|
<p class="mt-1 text-sm text-muted">
|
||||||
Use your local region and timezone for accurate dashboards and
|
Use your local region and timezone for accurate dashboards and
|
||||||
leaderboards.
|
leaderboards.
|
||||||
|
|
@ -51,7 +57,10 @@
|
||||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||||
|
|
||||||
<div>
|
<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
|
Country
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -83,12 +92,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" variant="primary">Save region settings</Button>
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Save region settings
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -118,12 +122,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" variant="primary">Save username</Button>
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Save username
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{#if badges.profile_url}
|
{#if badges.profile_url}
|
||||||
|
|
@ -150,7 +149,11 @@
|
||||||
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
<input type="hidden" name="authenticity_token" value={csrfToken} />
|
||||||
|
|
||||||
<label class="flex items-center gap-3 text-sm text-surface-content">
|
<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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="user[allow_public_stats_lookup]"
|
name="user[allow_public_stats_lookup]"
|
||||||
|
|
@ -161,12 +164,7 @@
|
||||||
Allow public stats lookup
|
Allow public stats lookup
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" variant="primary">Save privacy settings</Button>
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Save privacy settings
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -200,11 +198,15 @@
|
||||||
|
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<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>
|
<p class="mt-1 text-xs text-muted">{theme.description}</p>
|
||||||
</div>
|
</div>
|
||||||
{#if isSelected}
|
{#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
|
Selected
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -223,26 +225,36 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 grid grid-cols-[1fr_auto] items-center gap-2">
|
<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
|
||||||
<span class="h-2 w-8 rounded" style={`background:${theme.preview.darkless};`}></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>
|
||||||
|
|
||||||
<div class="mt-2 flex gap-1.5">
|
<div class="mt-2 flex gap-1.5">
|
||||||
<span class="h-1.5 w-6 rounded" style={`background:${theme.preview.info};`}></span>
|
<span
|
||||||
<span class="h-1.5 w-6 rounded" style={`background:${theme.preview.success};`}></span>
|
class="h-1.5 w-6 rounded"
|
||||||
<span class="h-1.5 w-6 rounded" style={`background:${theme.preview.warning};`}></span>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button type="submit" variant="primary">Save theme</Button>
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
Save theme
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
}: SettingsCommonProps & { children?: Snippet } = $props();
|
}: SettingsCommonProps & { children?: Snippet } = $props();
|
||||||
|
|
||||||
const sections = $derived(buildSections(section_paths, admin_tools.visible));
|
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) =>
|
const sectionButtonClass = (sectionId: keyof SectionPaths) =>
|
||||||
`block w-full px-4 py-4 text-left transition-colors ${
|
`block w-full px-4 py-4 text-left transition-colors ${
|
||||||
|
|
@ -32,7 +34,9 @@
|
||||||
if (!section || !knownSectionIds.has(section)) return;
|
if (!section || !knownSectionIds.has(section)) return;
|
||||||
if (section === active_section || !section_paths[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();
|
syncSectionFromHash();
|
||||||
|
|
@ -52,7 +56,9 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if errors.full_messages.length > 0}
|
{#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>
|
<p class="font-semibold">Some changes could not be saved:</p>
|
||||||
<ul class="mt-2 list-disc pl-5">
|
<ul class="mt-2 list-disc pl-5">
|
||||||
{#each errors.full_messages as message}
|
{#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)]">
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-[260px_minmax(0,1fr)]">
|
||||||
<aside class="h-max lg:sticky lg:top-8">
|
<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}
|
{#each sections as section}
|
||||||
<Link href={section.path} class={sectionButtonClass(section.id)}>
|
<Link href={section.path} class={sectionButtonClass(section.id)}>
|
||||||
<p class="text-sm font-semibold">{section.label}</p>
|
<p class="text-sm font-semibold">{section.label}</p>
|
||||||
|
|
|
||||||
|
|
@ -112,15 +112,14 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-center py-2">
|
<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">
|
<p class="text-secondary text-sm mb-6">
|
||||||
Heartbeat detected {heartbeatTimeAgo}.
|
Heartbeat detected {heartbeatTimeAgo}.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button
|
<Button href="/my/wakatime_setup/step-2" size="lg">
|
||||||
href="/my/wakatime_setup/step-2"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
Continue to Step 2 →
|
Continue to Step 2 →
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -225,6 +224,7 @@
|
||||||
class="mt-4 rounded-lg overflow-hidden border border-darkless"
|
class="mt-4 rounded-lg overflow-hidden border border-darkless"
|
||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
|
title="macOS setup video tutorial"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="300"
|
height="300"
|
||||||
src="https://www.youtube.com/embed/QTwhJy7nT_w?modestbranding=1&rel=0"
|
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"
|
class="mt-4 rounded-lg overflow-hidden border border-darkless"
|
||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
|
title="Windows setup video tutorial"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="300"
|
height="300"
|
||||||
src="https://www.youtube.com/embed/fX9tsiRvzhg?modestbranding=1&rel=0"
|
src="https://www.youtube.com/embed/fX9tsiRvzhg?modestbranding=1&rel=0"
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,33 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Link } from '@inertiajs/svelte';
|
import { Link } from "@inertiajs/svelte";
|
||||||
import Stepper from './Stepper.svelte';
|
import Stepper from "./Stepper.svelte";
|
||||||
|
|
||||||
const editors = [
|
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: "vscode",
|
||||||
{ id: 'neovim', name: 'Neovim', icon: '/images/editor-icons/neovim-128.png' },
|
name: "VS Code",
|
||||||
{ id: 'emacs', name: 'Emacs', icon: '/images/editor-icons/emacs-128.png' },
|
icon: "/images/editor-icons/vs-code-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: "vim", name: "Vim", icon: "/images/editor-icons/vim-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: "neovim",
|
||||||
{ id: 'other', name: 'Other', icon: null, emoji: '🔧' }
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
@ -25,22 +41,36 @@
|
||||||
|
|
||||||
<div class="text-center mb-10">
|
<div class="text-center mb-10">
|
||||||
<h2 class="text-2xl font-bold mb-2">Choose your editor</h2>
|
<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>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||||
{#each editors as editor}
|
{#each editors as editor}
|
||||||
<Link
|
<Link
|
||||||
href={`/my/wakatime_setup/step-3?editor=${editor.id}`}
|
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">
|
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">
|
>
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 mb-4 flex items-center justify-center transition-transform duration-200 group-hover:scale-110"
|
||||||
|
>
|
||||||
{#if editor.icon}
|
{#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}
|
{: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}
|
{/if}
|
||||||
</div>
|
</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>
|
</Link>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,8 @@
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 pl-6">
|
<div class="mt-4 pl-6">
|
||||||
<p class="text-sm mb-3 text-secondary">
|
<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>
|
</p>
|
||||||
<img
|
<img
|
||||||
src="/images/editor-toolbars/vs-code.png"
|
src="/images/editor-toolbars/vs-code.png"
|
||||||
|
|
@ -349,11 +350,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button href="/my/wakatime_setup/step-4" size="xl" class="w-full">
|
||||||
href="/my/wakatime_setup/step-4"
|
|
||||||
size="xl"
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
Next Step
|
Next Step
|
||||||
</Button>
|
</Button>
|
||||||
{:else if editor === "jetbrains"}
|
{:else if editor === "jetbrains"}
|
||||||
|
|
@ -367,7 +364,8 @@
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-semibold">Set Up JetBrains IDEs</h3>
|
<h3 class="text-xl font-semibold">Set Up JetBrains IDEs</h3>
|
||||||
<p class="text-secondary text-sm">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -386,7 +384,9 @@
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium mb-1">Open Settings</p>
|
<p class="font-medium mb-1">Open Settings</p>
|
||||||
<p class="text-sm text-secondary">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -403,11 +403,11 @@
|
||||||
Search for <b>WakaTime</b> in the marketplace and click Install.
|
Search for <b>WakaTime</b> in the marketplace and click Install.
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://plugins.jetbrains.com/plugin/7425-wakatime"
|
href="https://plugins.jetbrains.com/plugin/7425-wakatime"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="text-cyan hover:underline">View on Marketplace</a
|
class="text-cyan hover:underline">View on Marketplace</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -449,7 +449,8 @@
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 pl-6">
|
<div class="mt-4 pl-6">
|
||||||
<p class="text-sm mb-3 text-secondary">
|
<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>
|
</p>
|
||||||
<img
|
<img
|
||||||
src="/images/editor-toolbars/jetbrains.png"
|
src="/images/editor-toolbars/jetbrains.png"
|
||||||
|
|
@ -462,11 +463,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button href="/my/wakatime_setup/step-4" size="xl" class="w-full">
|
||||||
href="/my/wakatime_setup/step-4"
|
|
||||||
size="xl"
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
Next Step
|
Next Step
|
||||||
</Button>
|
</Button>
|
||||||
{:else if editor === "sublime"}
|
{:else if editor === "sublime"}
|
||||||
|
|
@ -515,13 +512,17 @@
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium mb-1">Install WakaTime Plugin</p>
|
<p class="font-medium mb-1">Install WakaTime Plugin</p>
|
||||||
<p class="text-sm text-secondary">
|
<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.
|
Open the Command Palette (Ctrl+Shift+P on Windows/Linux,
|
||||||
<a
|
Command+Shift+P on macOS), type <b
|
||||||
href="https://packagecontrol.io/packages/WakaTime"
|
>Package Control: Install Package</b
|
||||||
target="_blank"
|
>, and press Enter. Then type <b>WakaTime</b> and press Enter to
|
||||||
rel="noopener noreferrer"
|
install.
|
||||||
class="text-cyan hover:underline">View on Package Control</a
|
<a
|
||||||
>
|
href="https://packagecontrol.io/packages/WakaTime"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-cyan hover:underline">View on Package Control</a
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -535,7 +536,8 @@
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium mb-1">Start Coding</p>
|
<p class="font-medium mb-1">Start Coding</p>
|
||||||
<p class="text-sm text-secondary">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -562,7 +564,8 @@
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 pl-6">
|
<div class="mt-4 pl-6">
|
||||||
<p class="text-sm mb-3 text-secondary">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
@ -570,11 +573,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button href="/my/wakatime_setup/step-4" size="xl" class="w-full">
|
||||||
href="/my/wakatime_setup/step-4"
|
|
||||||
size="xl"
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
Next Step
|
Next Step
|
||||||
</Button>
|
</Button>
|
||||||
{:else if editorData[editor]}
|
{:else if editorData[editor]}
|
||||||
|
|
@ -601,7 +600,9 @@
|
||||||
<div class="pt-6 border-t border-darkless"></div>
|
<div class="pt-6 border-t border-darkless"></div>
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<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="relative group">
|
||||||
<div
|
<div
|
||||||
class="bg-darker border border-darkless rounded-lg overflow-x-auto"
|
class="bg-darker border border-darkless rounded-lg overflow-x-auto"
|
||||||
|
|
@ -620,11 +621,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button href="/my/wakatime_setup/step-4" size="xl" class="w-full">
|
||||||
href="/my/wakatime_setup/step-4"
|
|
||||||
size="xl"
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
Next Step
|
Next Step
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -697,11 +694,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button href="/my/wakatime_setup/step-4" size="xl" class="w-full">
|
||||||
href="/my/wakatime_setup/step-4"
|
|
||||||
size="xl"
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
Next Step
|
Next Step
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -9,29 +9,49 @@
|
||||||
{ number: 1, label: "Install" },
|
{ number: 1, label: "Install" },
|
||||||
{ number: 2, label: "Editor" },
|
{ number: 2, label: "Editor" },
|
||||||
{ number: 3, label: "Plugin" },
|
{ number: 3, label: "Plugin" },
|
||||||
{ number: 4, label: "Finish" }
|
{ number: 4, label: "Finish" },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-10">
|
<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>
|
<div class="absolute top-5 left-0 w-full h-0.5 bg-darkless -z-10"></div>
|
||||||
|
|
||||||
{#each steps as step}
|
{#each steps as step}
|
||||||
<div class="flex flex-col items-center gap-2 bg-darker z-10 px-2">
|
<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
|
<div
|
||||||
{currentStep > step.number ? 'bg-green border-green text-darker' :
|
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-primary border-primary text-on-primary' :
|
{currentStep > step.number
|
||||||
'bg-dark border-darkless text-secondary'}">
|
? '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}
|
{#if currentStep > step.number}
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
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>
|
</svg>
|
||||||
{:else}
|
{:else}
|
||||||
{step.number}
|
{step.number}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</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": "^7.3.1",
|
||||||
"vite-plugin-ruby": "^5.1.2",
|
"vite-plugin-ruby": "^5.1.2",
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-svelte": "^3.4.1",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|
@ -489,6 +493,10 @@
|
||||||
|
|
||||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
"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=="],
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"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.
|
# While tests run files are not watched, reloading is not necessary.
|
||||||
config.enable_reloading = false
|
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,
|
# 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
|
# 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
|
# 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 :project_durations
|
||||||
get :currently_hacking
|
get :currently_hacking
|
||||||
get :currently_hacking_count
|
get :currently_hacking_count
|
||||||
get :filterable_dashboard_content
|
|
||||||
get :filterable_dashboard
|
|
||||||
get :streak
|
get :streak
|
||||||
# get :timeline # Removed: Old route for timeline
|
# get :timeline # Removed: Old route for timeline
|
||||||
end
|
end
|
||||||
|
|
@ -204,8 +202,6 @@ Rails.application.routes.draw do
|
||||||
post :claim, on: :collection
|
post :claim, on: :collection
|
||||||
end
|
end
|
||||||
|
|
||||||
get "dashboard_stats", to: "dashboard_stats#show"
|
|
||||||
|
|
||||||
namespace :my do
|
namespace :my do
|
||||||
get "heartbeats/most_recent", to: "heartbeats#most_recent"
|
get "heartbeats/most_recent", to: "heartbeats#most_recent"
|
||||||
get "heartbeats", to: "heartbeats#index"
|
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,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|
@ -24,6 +24,10 @@
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-ruby": "^5.1.2"
|
"vite-plugin-ruby": "^5.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-svelte": "^3.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
|
@ -507,37 +511,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@inertiajs/core": {
|
"node_modules/@inertiajs/core": {
|
||||||
"version": "2.3.14",
|
"resolved": "vendor/inertia/packages/core",
|
||||||
"resolved": "file:vendor/inertia/packages/core",
|
"link": true
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/@inertiajs/svelte": {
|
"node_modules/@inertiajs/svelte": {
|
||||||
"version": "2.3.14",
|
"resolved": "vendor/inertia/packages/svelte",
|
||||||
"resolved": "file:vendor/inertia/packages/svelte",
|
"link": true
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
|
|
@ -1116,6 +1095,7 @@
|
||||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||||
"version": "6.2.4",
|
"version": "6.2.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
|
|
@ -1443,6 +1423,7 @@
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -1505,6 +1486,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.15.11",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
|
|
@ -2967,6 +2949,7 @@
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -3024,6 +3007,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -3172,6 +3156,34 @@
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.51.2.tgz",
|
||||||
"integrity": "sha512-AqApqNOxVS97V4Ko9UHTHeSuDJrwauJhZpLDs1gYD8Jk48ntCSWD7NxKje+fnGn5Ja1O3u2FzQZHPdifQjXe3w==",
|
"integrity": "sha512-AqApqNOxVS97V4Ko9UHTHeSuDJrwauJhZpLDs1gYD8Jk48ntCSWD7NxKje+fnGn5Ja1O3u2FzQZHPdifQjXe3w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
|
|
@ -3453,7 +3466,8 @@
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.18",
|
"version": "4.1.18",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
|
|
@ -3526,6 +3540,7 @@
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -3541,6 +3556,7 @@
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -3652,6 +3668,39 @@
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"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,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"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": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
|
|
@ -24,5 +27,9 @@
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-ruby": "^5.1.2"
|
"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