Inertia migration/UI3 (#911)
* Inertia p1? * Inertia'fied signed out homepage? * Split up signed in page * WIP signed in v2? * Better signed in? * Clean up extensions page! * Fix currently hacking * Better docs page? * Docs update 2 * Clean up "What is Hackatime?" + get rid of that godawful green dev mode * Better nav? * Cleaner settings? * Fix commit times * Fix flashes + OS improv * Setup v2 * Readd some of the syncers? * Remove stray emdash * Clean up Step 3 * Oops, remove .vite * bye bye, /inertia-example * bin/rubocop -A * Fix docs vuln
6
.gitignore
vendored
|
|
@ -43,3 +43,9 @@
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
public/vite-dev
|
||||||
|
public/vite-ssr
|
||||||
|
|
||||||
|
.vite
|
||||||
|
|
@ -26,6 +26,10 @@ RUN apt-get update -qq && \
|
||||||
wget && \
|
wget && \
|
||||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
|
# Install Bun
|
||||||
|
RUN curl -fsSL https://bun.sh/install | bash && \
|
||||||
|
mv ~/.bun/bin/bun /usr/local/bin/
|
||||||
|
|
||||||
# Set production environment
|
# Set production environment
|
||||||
ENV RAILS_ENV="production" \
|
ENV RAILS_ENV="production" \
|
||||||
BUNDLE_DEPLOYMENT="1" \
|
BUNDLE_DEPLOYMENT="1" \
|
||||||
|
|
@ -40,6 +44,10 @@ RUN apt-get update -qq && \
|
||||||
apt-get install --no-install-recommends -y build-essential git pkg-config libpq-dev libyaml-dev && \
|
apt-get install --no-install-recommends -y build-essential git pkg-config libpq-dev libyaml-dev && \
|
||||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||||
|
|
||||||
|
# Install npm dependencies for Vite
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun i --frozen-lockfile
|
||||||
|
|
||||||
# Install application gems
|
# Install application gems
|
||||||
COPY Gemfile Gemfile.lock ./
|
COPY Gemfile Gemfile.lock ./
|
||||||
RUN bundle install && \
|
RUN bundle install && \
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
FROM ruby:3.4.8
|
FROM ruby:3.4.8
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies including Node.js
|
||||||
RUN apt-get update -qq && \
|
RUN apt-get update -qq && \
|
||||||
apt-get install --no-install-recommends -y \
|
apt-get install --no-install-recommends -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
|
@ -12,11 +12,21 @@ RUN apt-get update -qq && \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
curl \
|
curl \
|
||||||
vim \
|
vim \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives
|
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives
|
||||||
|
|
||||||
|
# Install Bun
|
||||||
|
RUN curl -fsSL https://bun.sh/install | bash && \
|
||||||
|
mv ~/.bun/bin/bun /usr/local/bin/
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install npm dependencies for Vite
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
# Install application dependencies
|
# Install application dependencies
|
||||||
COPY Gemfile Gemfile.lock ./
|
COPY Gemfile Gemfile.lock ./
|
||||||
RUN bundle install
|
RUN bundle install
|
||||||
|
|
|
||||||
4
Gemfile
|
|
@ -160,3 +160,7 @@ gem "autotuner", "~> 1.0"
|
||||||
gem "tailwindcss-ruby", "~> 4.1"
|
gem "tailwindcss-ruby", "~> 4.1"
|
||||||
|
|
||||||
gem "tailwindcss-rails", "~> 4.2"
|
gem "tailwindcss-rails", "~> 4.2"
|
||||||
|
|
||||||
|
gem "inertia_rails", "~> 3.17"
|
||||||
|
|
||||||
|
gem "vite_rails", "~> 3.0"
|
||||||
|
|
|
||||||
17
Gemfile.lock
|
|
@ -136,6 +136,7 @@ GEM
|
||||||
dotenv (= 3.2.0)
|
dotenv (= 3.2.0)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
drb (2.2.3)
|
drb (2.2.3)
|
||||||
|
dry-cli (1.4.1)
|
||||||
ed25519 (1.4.0)
|
ed25519 (1.4.0)
|
||||||
erb (6.0.1)
|
erb (6.0.1)
|
||||||
erb_lint (0.9.0)
|
erb_lint (0.9.0)
|
||||||
|
|
@ -221,6 +222,8 @@ GEM
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
activesupport (>= 6.0.0)
|
activesupport (>= 6.0.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
|
inertia_rails (3.17.0)
|
||||||
|
railties (>= 6)
|
||||||
io-console (0.8.2)
|
io-console (0.8.2)
|
||||||
irb (1.16.0)
|
irb (1.16.0)
|
||||||
pp (>= 0.6.0)
|
pp (>= 0.6.0)
|
||||||
|
|
@ -278,6 +281,7 @@ GEM
|
||||||
prism (~> 1.5)
|
prism (~> 1.5)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.4.1)
|
||||||
|
mutex_m (0.3.0)
|
||||||
net-http (0.9.1)
|
net-http (0.9.1)
|
||||||
uri (>= 0.11.1)
|
uri (>= 0.11.1)
|
||||||
net-http-persistent (4.0.8)
|
net-http-persistent (4.0.8)
|
||||||
|
|
@ -370,6 +374,8 @@ GEM
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
logger (>= 1.6.0)
|
logger (>= 1.6.0)
|
||||||
rack (>= 3.0.0, < 4)
|
rack (>= 3.0.0, < 4)
|
||||||
|
rack-proxy (0.7.7)
|
||||||
|
rack
|
||||||
rack-session (2.1.1)
|
rack-session (2.1.1)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
|
|
@ -561,6 +567,15 @@ GEM
|
||||||
uniform_notifier (1.18.0)
|
uniform_notifier (1.18.0)
|
||||||
uri (1.1.1)
|
uri (1.1.1)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
|
vite_rails (3.0.20)
|
||||||
|
railties (>= 5.1, < 9)
|
||||||
|
vite_ruby (~> 3.0, >= 3.2.2)
|
||||||
|
vite_ruby (3.9.2)
|
||||||
|
dry-cli (>= 0.7, < 2)
|
||||||
|
logger (~> 1.6)
|
||||||
|
mutex_m
|
||||||
|
rack-proxy (~> 0.6, >= 0.6.1)
|
||||||
|
zeitwerk (~> 2.2)
|
||||||
web-console (4.2.1)
|
web-console (4.2.1)
|
||||||
actionview (>= 6.0.0)
|
actionview (>= 6.0.0)
|
||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
|
|
@ -612,6 +627,7 @@ DEPENDENCIES
|
||||||
htmlcompressor (~> 0.4.0)
|
htmlcompressor (~> 0.4.0)
|
||||||
http
|
http
|
||||||
importmap-rails
|
importmap-rails
|
||||||
|
inertia_rails (~> 3.17)
|
||||||
jbuilder
|
jbuilder
|
||||||
kamal
|
kamal
|
||||||
letter_opener
|
letter_opener
|
||||||
|
|
@ -652,6 +668,7 @@ DEPENDENCIES
|
||||||
thruster
|
thruster
|
||||||
turbo-rails
|
turbo-rails
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
|
vite_rails (~> 3.0)
|
||||||
web-console
|
web-console
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
web: bin/rails server -b 0.0.0.0
|
web: bin/rails server -b 0.0.0.0
|
||||||
css: bin/rails tailwindcss:watch
|
css: bin/rails tailwindcss:watch
|
||||||
|
vite: bin/vite dev
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@source "../../../app/javascript/**/*.{svelte,js,ts}";
|
||||||
|
@source "../../../node_modules/layerchart/dist/**/*.{svelte,js}";
|
||||||
@import "./main.css";
|
@import "./main.css";
|
||||||
@import "./nav.css";
|
@import "./nav.css";
|
||||||
@import "./filterable_dashboard.css";
|
@import "./filterable_dashboard.css";
|
||||||
|
|
@ -18,22 +20,31 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-darker: #121217;
|
--color-darker: #1f1617;
|
||||||
--color-dark: #17171D;
|
--color-dark: #2a1f21;
|
||||||
--color-darkless: #252429;
|
--color-darkless: #4a2d31;
|
||||||
--color-red: #EC3750;
|
--color-red: #c8394f;
|
||||||
--color-orange: #FF8C37;
|
--color-orange: #ff8c37;
|
||||||
--color-yellow: #F1C40F;
|
--color-yellow: #f1c40f;
|
||||||
--color-green: #33D6A6;
|
--color-green: #33d6a6;
|
||||||
--color-cyan: #5BC0DE;
|
--color-cyan: #5bc0de;
|
||||||
--color-blue: #338EDA;
|
--color-blue: #338eda;
|
||||||
--color-purple: #A633D6;
|
--color-purple: #a633d6;
|
||||||
--color-primary: #EC3750;
|
--color-primary: #c8394f;
|
||||||
--color-secondary: #8492A6;
|
--color-secondary: #6e6468;
|
||||||
}
|
|
||||||
|
|
||||||
:root.development {
|
--color-muted: #6e6468;
|
||||||
--color-primary: var(--color-green);
|
--color-text-muted: #6e6468;
|
||||||
|
|
||||||
|
--color-surface: #2a1f21;
|
||||||
|
--color-surface-100: #3a292d;
|
||||||
|
--color-surface-200: #4a3438;
|
||||||
|
--color-surface-300: #5a3f44;
|
||||||
|
--color-surface-content: #f3ecee;
|
||||||
|
--color-info: #5bc0de;
|
||||||
|
--color-success: #33d6a6;
|
||||||
|
--color-warning: #f1c40f;
|
||||||
|
--color-danger: #c8394f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-toggle-group {
|
.project-toggle-group {
|
||||||
|
|
@ -72,4 +83,4 @@ main {
|
||||||
.interval-selector-button:hover {
|
.interval-selector-button:hover {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,8 @@
|
||||||
@font-face {
|
|
||||||
font-family: "Phantom Sans";
|
|
||||||
src: url("/fonts/Regular.woff2") format("woff2"), url("/fonts/Regular.woff") format("woff");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Phantom Sans";
|
|
||||||
src: url("/fonts/Italic.woff2") format("woff2"), url("/fonts/Italic.woff") format("woff");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: italic;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Phantom Sans";
|
|
||||||
src: url("/fonts/Bold.woff2") format("woff2"), url("/fonts/Bold.woff") format("woff");
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "DS-Digital";
|
|
||||||
src: url("/fonts/DS-DIGIB.ttf") format("truetype");
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "clockicons";
|
font-family: "clockicons";
|
||||||
src: url("/fonts/clockicons.woff2") format("woff2"), url("/fonts/clockicons.ttf") format("truetype");
|
src:
|
||||||
|
url("/fonts/clockicons.woff2") format("woff2"),
|
||||||
|
url("/fonts/clockicons.ttf") format("truetype");
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
@ -47,27 +17,29 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink-anim {
|
@keyframes blink-anim {
|
||||||
0%, 50% {
|
0%,
|
||||||
|
50% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
51%, 100% {
|
51%,
|
||||||
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body, html {
|
body,
|
||||||
font-family: "Phantom Sans", "Segoe UI", sans-serif;
|
html {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI",
|
||||||
|
sans-serif;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
*, *::before, *::after {
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ds-digital {
|
|
||||||
font-family: "DS-Digital", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clockicons {
|
.clockicons {
|
||||||
font-family: "clockicons", monospace;
|
font-family: "clockicons", monospace;
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +48,10 @@ body, html {
|
||||||
animation: blink-anim 1s infinite;
|
animation: blink-anim 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
code, pre, kbd, samp {
|
code,
|
||||||
|
pre,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
@apply font-mono;
|
@apply font-mono;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +60,9 @@ code, pre, kbd, samp {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure forms and inputs respect dark mode */
|
/* Ensure forms and inputs respect dark mode */
|
||||||
input, textarea, select {
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +92,9 @@ select {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-spin {
|
.animate-spin {
|
||||||
|
|
@ -151,7 +130,8 @@ turbo-frame#mini_leaderboard[aria-busy="true"]::before {
|
||||||
@apply rotate-x-180;
|
@apply rotate-x-180;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clock-display-front, .clock-display-back {
|
.clock-display-front,
|
||||||
|
.clock-display-back {
|
||||||
@apply absolute w-full h-full flex justify-center items-center backface-hidden;
|
@apply absolute w-full h-full flex justify-center items-center backface-hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,4 +164,4 @@ turbo-frame#mini_leaderboard[aria-busy="true"]::before {
|
||||||
|
|
||||||
.project-desc {
|
.project-desc {
|
||||||
@apply relative bg-[linear-gradient(145deg,#f6dbba_0%,#e6d4be_100%)] rounded-[clamp(6px,1.5vw,10px)] shadow-[0_4px_15px_rgba(0,0,0,0.15),inset_0_1px_0_rgba(255,255,255,0.6),inset_3px_0_5px_rgba(0,0,0,0.05)] transition-all duration-300 ease-in-out border-[2px] border-[rgba(89,47,49,0.3)] overflow-hidden text-black h-full cursor-pointer flex flex-col z-2 mx-5;
|
@apply relative bg-[linear-gradient(145deg,#f6dbba_0%,#e6d4be_100%)] rounded-[clamp(6px,1.5vw,10px)] shadow-[0_4px_15px_rgba(0,0,0,0.15),inset_0_1px_0_rgba(255,255,255,0.6),inset_3px_0_5px_rgba(0,0,0,0.05)] transition-all duration-300 ease-in-out border-[2px] border-[rgba(89,47,49,0.3)] overflow-hidden text-black h-full cursor-pointer flex flex-col z-2 mx-5;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,68 @@
|
||||||
class DocsController < ApplicationController
|
class DocsController < InertiaController
|
||||||
|
layout "inertia"
|
||||||
|
|
||||||
|
POPULAR_EDITORS = [
|
||||||
|
[ "VS Code", "vs-code" ], [ "PyCharm", "pycharm" ], [ "IntelliJ IDEA", "intellij-idea" ],
|
||||||
|
[ "Sublime Text", "sublime-text" ], [ "Vim", "vim" ], [ "Neovim", "neovim" ],
|
||||||
|
[ "Android Studio", "android-studio" ], [ "Xcode", "xcode" ], [ "Unity", "unity" ],
|
||||||
|
[ "Godot", "godot" ], [ "Cursor", "cursor" ], [ "Zed", "zed" ],
|
||||||
|
[ "Terminal", "terminal" ], [ "WebStorm", "webstorm" ], [ "Eclipse", "eclipse" ],
|
||||||
|
[ "Emacs", "emacs" ], [ "Jupyter", "jupyter" ], [ "OnShape", "onshape" ]
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
ALL_EDITORS = [
|
||||||
|
[ "Android Studio", "android-studio" ], [ "AppCode", "appcode" ], [ "Aptana", "aptana" ],
|
||||||
|
[ "Arduino IDE", "arduino-ide" ], [ "Azure Data Studio", "azure-data-studio" ],
|
||||||
|
[ "Blender", "blender" ], [ "Brackets", "brackets" ], [ "Brave", "brave" ],
|
||||||
|
[ "C++ Builder", "c++-builder" ], [ "Canva", "canva" ], [ "Chrome", "chrome" ],
|
||||||
|
[ "CLion", "clion" ], [ "Cloud9", "cloud9" ], [ "Coda", "coda" ],
|
||||||
|
[ "CodeTasty", "codetasty" ], [ "Cursor", "cursor" ], [ "DataGrip", "datagrip" ],
|
||||||
|
[ "DataSpell", "dataspell" ], [ "DBeaver", "dbeaver" ], [ "Delphi", "delphi" ],
|
||||||
|
[ "Discord", "discord" ], [ "Eclipse", "eclipse" ], [ "Edge", "edge" ],
|
||||||
|
[ "Emacs", "emacs" ], [ "Eric", "eric" ], [ "Excel", "excel" ],
|
||||||
|
[ "Figma", "figma" ], [ "Firefox", "firefox" ], [ "Gedit", "gedit" ],
|
||||||
|
[ "Godot", "godot" ], [ "GoLand", "goland" ], [ "HBuilder X", "hbuilder-x" ],
|
||||||
|
[ "IDA Pro", "ida-pro" ], [ "IntelliJ IDEA", "intellij-idea" ], [ "Jupyter", "jupyter" ],
|
||||||
|
[ "Kakoune", "kakoune" ], [ "Kate", "kate" ], [ "Komodo", "komodo" ],
|
||||||
|
[ "Micro", "micro" ], [ "MPS", "mps" ], [ "Neovim", "neovim" ],
|
||||||
|
[ "NetBeans", "netbeans" ], [ "Notepad++", "notepad++" ], [ "Nova", "nova" ],
|
||||||
|
[ "Obsidian", "obsidian" ], [ "OnShape", "onshape" ], [ "Oxygen", "oxygen" ],
|
||||||
|
[ "PhpStorm", "phpstorm" ], [ "Postman", "postman" ], [ "PowerPoint", "powerpoint" ],
|
||||||
|
[ "Processing", "processing" ], [ "Pulsar", "pulsar" ], [ "PyCharm", "pycharm" ],
|
||||||
|
[ "ReClassEx", "reclassex" ], [ "Rider", "rider" ], [ "Roblox Studio", "roblox-studio" ],
|
||||||
|
[ "RubyMine", "rubymine" ], [ "RustRover", "rustrover" ], [ "Safari", "safari" ],
|
||||||
|
[ "SiYuan", "siyuan" ], [ "Sketch", "sketch" ], [ "SlickEdit", "slickedit" ],
|
||||||
|
[ "SQL Server Management Studio", "sql-server-management-studio" ],
|
||||||
|
[ "Sublime Text", "sublime-text" ], [ "Terminal", "terminal" ],
|
||||||
|
[ "TeXstudio", "texstudio" ], [ "TextMate", "textmate" ], [ "Trae", "trae" ],
|
||||||
|
[ "Unity", "unity" ], [ "Unreal Engine 4", "unreal-engine-4" ],
|
||||||
|
[ "Vim", "vim" ], [ "Visual Studio", "visual-studio" ], [ "VS Code", "vs-code" ],
|
||||||
|
[ "WebStorm", "webstorm" ], [ "Windsurf", "windsurf" ], [ "Wing", "wing" ],
|
||||||
|
[ "Word", "word" ], [ "Xcode", "xcode" ], [ "Zed", "zed" ],
|
||||||
|
[ "Swift Playgrounds", "swift-playgrounds" ]
|
||||||
|
].sort_by { |editor| editor[0] }.freeze
|
||||||
|
|
||||||
# Docs are publicly accessible - no authentication required
|
# Docs are publicly accessible - no authentication required
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@docs = docs_structure
|
render inertia: "Docs/Index", props: {
|
||||||
|
popular_editors: POPULAR_EDITORS,
|
||||||
|
all_editors: ALL_EDITORS
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@doc_path = sanitize_path(params[:path] || "index")
|
doc_path = sanitize_path(params[:path] || "index")
|
||||||
|
|
||||||
if @doc_path.start_with?("api")
|
if doc_path.start_with?("api")
|
||||||
redirect_to "/api-docs", allow_other_host: false and return
|
redirect_to "/api-docs", allow_other_host: false and return
|
||||||
end
|
end
|
||||||
|
|
||||||
@breadcrumbs = build_breadcrumbs(@doc_path)
|
file_path = safe_docs_path("#{doc_path}.md")
|
||||||
|
|
||||||
file_path = safe_docs_path("#{@doc_path}.md")
|
|
||||||
|
|
||||||
unless File.exist?(file_path)
|
unless File.exist?(file_path)
|
||||||
# Try with index.md in the directory
|
# Try with index.md in the directory
|
||||||
dir_path = safe_docs_path(@doc_path, "index.md")
|
dir_path = safe_docs_path(doc_path, "index.md")
|
||||||
if File.exist?(dir_path)
|
if File.exist?(dir_path)
|
||||||
file_path = dir_path
|
file_path = dir_path
|
||||||
else
|
else
|
||||||
|
|
@ -26,9 +70,23 @@ class DocsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@content = read_docs_file(file_path)
|
content = read_docs_file(file_path)
|
||||||
@title = extract_title(@content) || @doc_path.humanize
|
title = extract_title(content) || doc_path.humanize
|
||||||
@rendered_content = render_markdown(@content)
|
rendered_content = render_markdown(content)
|
||||||
|
breadcrumbs = build_inertia_breadcrumbs(doc_path)
|
||||||
|
edit_url = "https://github.com/hackclub/hackatime/edit/main/docs/#{doc_path}.md"
|
||||||
|
|
||||||
|
render inertia: "Docs/Show", props: {
|
||||||
|
doc_path: doc_path,
|
||||||
|
title: title,
|
||||||
|
rendered_content: rendered_content,
|
||||||
|
breadcrumbs: breadcrumbs,
|
||||||
|
edit_url: edit_url,
|
||||||
|
meta: {
|
||||||
|
description: generate_doc_description(content, title),
|
||||||
|
keywords: generate_doc_keywords(doc_path, title)
|
||||||
|
}
|
||||||
|
}
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error "Error loading docs: #{e.message}"
|
Rails.logger.error "Error loading docs: #{e.message}"
|
||||||
render_not_found
|
render_not_found
|
||||||
|
|
@ -40,13 +98,9 @@ class DocsController < ApplicationController
|
||||||
# Remove any directory traversal attempts and normalize path
|
# Remove any directory traversal attempts and normalize path
|
||||||
return "index" if path.blank?
|
return "index" if path.blank?
|
||||||
|
|
||||||
# Remove leading/trailing slashes and dangerous characters
|
clean_path = path.to_s.split("/").reject(&:empty?).join("/").gsub("..", "")
|
||||||
clean_path = path.to_s.gsub(/\A\/+|\/+\z/, "").gsub(/\.\./, "")
|
|
||||||
|
|
||||||
# Only allow alphanumeric characters, hyphens, underscores, plus signs, and forward slashes
|
|
||||||
clean_path = clean_path.gsub(/[^a-zA-Z0-9\-_+\/]/, "")
|
clean_path = clean_path.gsub(/[^a-zA-Z0-9\-_+\/]/, "")
|
||||||
|
|
||||||
# Ensure we don't have empty path
|
|
||||||
clean_path.present? ? clean_path : "index"
|
clean_path.present? ? clean_path : "index"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -92,9 +146,9 @@ class DocsController < ApplicationController
|
||||||
structure
|
structure
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_breadcrumbs(path)
|
def build_inertia_breadcrumbs(path)
|
||||||
parts = path.split("/")
|
parts = path.split("/")
|
||||||
breadcrumbs = [ { name: "Docs", path: docs_path, is_link: true } ]
|
breadcrumbs = [ { name: "Docs", href: docs_path, is_link: true } ]
|
||||||
|
|
||||||
current_path = ""
|
current_path = ""
|
||||||
parts.each_with_index do |part, index|
|
parts.each_with_index do |part, index|
|
||||||
|
|
@ -105,10 +159,11 @@ class DocsController < ApplicationController
|
||||||
File.exist?(safe_docs_path(current_path, "index.md"))
|
File.exist?(safe_docs_path(current_path, "index.md"))
|
||||||
|
|
||||||
# Only make it a link if the file exists, or if it's the current page (last item)
|
# Only make it a link if the file exists, or if it's the current page (last item)
|
||||||
if file_exists || index == parts.length - 1
|
is_last = index == parts.length - 1
|
||||||
breadcrumbs << { name: part.titleize, path: doc_path(current_path), is_link: true }
|
if file_exists || is_last
|
||||||
|
breadcrumbs << { name: part.titleize, href: doc_path(current_path), is_link: !is_last }
|
||||||
else
|
else
|
||||||
breadcrumbs << { name: part.titleize, path: nil, is_link: false }
|
breadcrumbs << { name: part.titleize, href: nil, is_link: false }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -159,10 +214,11 @@ class DocsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_not_found
|
def render_not_found
|
||||||
@status_code = 404
|
render inertia: "Errors/NotFound", props: {
|
||||||
@title = "Page Not Found"
|
status_code: 404,
|
||||||
@message = "The documentation page you were looking for doesn't exist."
|
title: "Page Not Found",
|
||||||
render "errors/show", status: :not_found, layout: "errors"
|
message: "The documentation page you were looking for doesn't exist."
|
||||||
|
}, status: :not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
# Make these helper methods available to views
|
# Make these helper methods available to views
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
class ExtensionsController < ApplicationController
|
class ExtensionsController < InertiaController
|
||||||
|
layout "inertia"
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
render inertia: "Extensions/Index"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
161
app/controllers/inertia_controller.rb
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class InertiaController < ApplicationController
|
||||||
|
inertia_share layout: -> { inertia_layout_props }
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def inertia_layout_props
|
||||||
|
{
|
||||||
|
nav: inertia_nav_props,
|
||||||
|
footer: inertia_footer_props,
|
||||||
|
currently_hacking: currently_hacking_props,
|
||||||
|
csrf_token: form_authenticity_token,
|
||||||
|
signout_path: signout_path,
|
||||||
|
show_stop_impersonating: session[:impersonater_user_id].present?,
|
||||||
|
stop_impersonating_path: stop_impersonating_path
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def inertia_nav_props
|
||||||
|
{
|
||||||
|
flash: inertia_flash_messages,
|
||||||
|
user_present: current_user.present?,
|
||||||
|
user_mention_html: current_user ? render_to_string(partial: "shared/user_mention", locals: { user: current_user }) : nil,
|
||||||
|
streak_html: current_user ? render_to_string(partial: "static_pages/streak", locals: { user: current_user, show_text: true, turbo_frame: false }) : nil,
|
||||||
|
admin_level_html: current_user ? render_to_string(partial: "static_pages/admin_level", locals: { user: current_user }) : nil,
|
||||||
|
login_path: slack_auth_path,
|
||||||
|
links: inertia_primary_links,
|
||||||
|
dev_links: inertia_dev_links,
|
||||||
|
admin_links: inertia_admin_links,
|
||||||
|
viewer_links: inertia_viewer_links,
|
||||||
|
superadmin_links: inertia_superadmin_links,
|
||||||
|
activities_html: inertia_activities_html
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def inertia_flash_messages
|
||||||
|
flash.to_hash.map do |type, message|
|
||||||
|
{
|
||||||
|
message: message.to_s,
|
||||||
|
class_name: type.to_sym == :notice ? "border-green text-green" : "border-primary text-primary"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def inertia_primary_links
|
||||||
|
links = []
|
||||||
|
links << inertia_link("Home", root_path, active: helpers.current_page?(root_path))
|
||||||
|
links << inertia_link("Leaderboards", leaderboards_path, active: helpers.current_page?(leaderboards_path))
|
||||||
|
|
||||||
|
if current_user
|
||||||
|
links << inertia_link("Projects", my_projects_path, active: helpers.current_page?(my_projects_path))
|
||||||
|
links << inertia_link("Docs", docs_path, active: helpers.current_page?(docs_path) || request.path.start_with?("/docs"))
|
||||||
|
links << inertia_link("Extensions", extensions_path, active: helpers.current_page?(extensions_path))
|
||||||
|
links << inertia_link("Settings", my_settings_path, active: helpers.current_page?(my_settings_path))
|
||||||
|
links << inertia_link("My OAuth Apps", oauth_applications_path, active: helpers.current_page?(oauth_applications_path) || request.path.start_with?("/oauth/applications"))
|
||||||
|
links << { label: "Logout", action: "logout" }
|
||||||
|
else
|
||||||
|
links << inertia_link("Docs", docs_path, active: helpers.current_page?(docs_path) || request.path.start_with?("/docs"))
|
||||||
|
links << inertia_link("Extensions", extensions_path, active: helpers.current_page?(extensions_path))
|
||||||
|
links << inertia_link("What is Hackatime?", "/what-is-hackatime", active: helpers.current_page?("/what-is-hackatime"))
|
||||||
|
end
|
||||||
|
|
||||||
|
links
|
||||||
|
end
|
||||||
|
|
||||||
|
def inertia_dev_links
|
||||||
|
return [] unless Rails.env.development?
|
||||||
|
|
||||||
|
[
|
||||||
|
inertia_link("Letter Opener", letter_opener_web_path, active: helpers.current_page?(letter_opener_web_path)),
|
||||||
|
inertia_link("Mailers", "/rails/mailers", active: helpers.current_page?("/rails/mailers"))
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def inertia_admin_links
|
||||||
|
return [] unless current_user&.admin_level.in?(%w[admin superadmin])
|
||||||
|
|
||||||
|
links = []
|
||||||
|
links << inertia_link("Review Timeline", admin_timeline_path, active: helpers.current_page?(admin_timeline_path))
|
||||||
|
links << inertia_link("Trust Level Logs", admin_trust_level_audit_logs_path, active: helpers.current_page?(admin_trust_level_audit_logs_path) || request.path.start_with?("/admin/trust_level_audit_logs"))
|
||||||
|
links << inertia_link("Admin API Keys", admin_admin_api_keys_path, active: helpers.current_page?(admin_admin_api_keys_path) || request.path.start_with?("/admin/admin_api_keys"))
|
||||||
|
links
|
||||||
|
end
|
||||||
|
|
||||||
|
def inertia_viewer_links
|
||||||
|
return [] unless current_user&.admin_level == "viewer"
|
||||||
|
|
||||||
|
[
|
||||||
|
inertia_link("Review Timeline", admin_timeline_path, active: helpers.current_page?(admin_timeline_path)),
|
||||||
|
inertia_link("Trust Level Logs", admin_trust_level_audit_logs_path, active: helpers.current_page?(admin_trust_level_audit_logs_path) || request.path.start_with?("/admin/trust_level_audit_logs")),
|
||||||
|
inertia_link("Admin API Keys", admin_admin_api_keys_path, active: helpers.current_page?(admin_admin_api_keys_path) || request.path.start_with?("/admin/admin_api_keys"))
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def inertia_superadmin_links
|
||||||
|
return [] unless current_user&.admin_level == "superadmin"
|
||||||
|
|
||||||
|
links = []
|
||||||
|
links << inertia_link("Admin Management", admin_admin_users_path, active: helpers.current_page?(admin_admin_users_path))
|
||||||
|
pending_count = DeletionRequest.pending.count
|
||||||
|
links << inertia_link("Account Deletions", admin_deletion_requests_path, active: helpers.current_page?(admin_deletion_requests_path), badge: pending_count.positive? ? pending_count : nil)
|
||||||
|
links << inertia_link("GoodBoy", good_job_path, active: helpers.current_page?(good_job_path))
|
||||||
|
links << inertia_link("All OAuth Apps", admin_oauth_applications_path, active: helpers.current_page?(admin_oauth_applications_path) || request.path.start_with?("/admin/oauth_applications"))
|
||||||
|
links << inertia_link("Feature Flags", flipper_path, active: helpers.current_page?(flipper_path))
|
||||||
|
links
|
||||||
|
end
|
||||||
|
|
||||||
|
def inertia_link(label, href, active: false, badge: nil)
|
||||||
|
{ label: label, href: href, active: active, badge: badge }
|
||||||
|
end
|
||||||
|
|
||||||
|
def inertia_activities_html
|
||||||
|
return nil unless defined?(@activities) && @activities.present?
|
||||||
|
helpers.render_activities(@activities)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inertia_footer_props
|
||||||
|
helpers = ApplicationController.helpers
|
||||||
|
cache = helpers.cache_stats
|
||||||
|
hours = active_users_graph_data.map.with_index do |entry, index|
|
||||||
|
{
|
||||||
|
height: entry[:height],
|
||||||
|
title: "#{helpers.pluralize(index + 1, 'hour')} ago, #{helpers.pluralize(entry[:users], 'people')} logged time. '#{FlavorText.latin_phrases.sample}.'"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
git_version: Rails.application.config.git_version,
|
||||||
|
commit_link: Rails.application.config.commit_link,
|
||||||
|
server_start_time_ago: helpers.time_ago_in_words(Rails.application.config.server_start_time),
|
||||||
|
heartbeat_recent_count: Heartbeat.recent_count,
|
||||||
|
heartbeat_recent_imported_count: Heartbeat.recent_imported_count,
|
||||||
|
query_count: QueryCount::Counter.counter,
|
||||||
|
query_cache_count: QueryCount::Counter.counter_cache,
|
||||||
|
cache_hits: cache[:hits],
|
||||||
|
cache_misses: cache[:misses],
|
||||||
|
requests_per_second: helpers.requests_per_second,
|
||||||
|
active_users_graph: hours
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def currently_hacking_props
|
||||||
|
data = Cache::CurrentlyHackingJob.perform_now
|
||||||
|
users = (data[:users] || []).map do |u|
|
||||||
|
proj = data[:active_projects]&.dig(u.id)
|
||||||
|
{
|
||||||
|
id: u.id,
|
||||||
|
display_name: u.display_name,
|
||||||
|
slack_uid: u.slack_uid,
|
||||||
|
avatar_url: u.avatar_url,
|
||||||
|
active_project: proj && { name: proj.project_name, repo_url: proj.repo_url }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
{
|
||||||
|
count: users.size,
|
||||||
|
users: users,
|
||||||
|
interval: 30_000
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -151,7 +151,8 @@ class SessionsController < ApplicationController
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
HandleEmailSigninJob.perform_later(email, continue_param)
|
HandleEmailSigninJob.perform_later(email, continue_param)
|
||||||
else
|
else
|
||||||
HandleEmailSigninJob.perform_now(email, continue_param)
|
token = HandleEmailSigninJob.perform_now(email, continue_param)
|
||||||
|
session[:dev_magic_link] = auth_token_url(token)
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to root_path(sign_in_email: true), notice: "Check your email for a sign-in link!"
|
redirect_to root_path(sign_in_email: true), notice: "Check your email for a sign-in link!"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
class StaticPagesController < ApplicationController
|
class StaticPagesController < InertiaController
|
||||||
|
layout "inertia", only: :index
|
||||||
before_action :ensure_current_user, only: %i[
|
before_action :ensure_current_user, only: %i[
|
||||||
filterable_dashboard
|
filterable_dashboard
|
||||||
filterable_dashboard_content
|
filterable_dashboard_content
|
||||||
|
|
@ -15,7 +16,7 @@ class StaticPagesController < ApplicationController
|
||||||
return redirect_to "/my/projects?interval=custom&from=#{d}&to=#{d}" if d
|
return redirect_to "/my/projects?interval=custom&from=#{d}&to=#{d}" if d
|
||||||
end
|
end
|
||||||
|
|
||||||
if current_user.heartbeats.empty? || params[:show_wakatime_setup_notice]
|
if !current_user.heartbeats.exists? || params[:show_wakatime_setup_notice]
|
||||||
@show_wakatime_setup_notice = true
|
@show_wakatime_setup_notice = true
|
||||||
if (ssp = Cache::SetupSocialProofJob.perform_now)
|
if (ssp = Cache::SetupSocialProofJob.perform_now)
|
||||||
@ssp_message, @ssp_users_recent, @ssp_users_size = ssp.values_at(:message, :users_recent, :users_size)
|
@ssp_message, @ssp_users_recent, @ssp_users_size = ssp.values_at(:message, :users_recent, :users_size)
|
||||||
|
|
@ -46,6 +47,8 @@ class StaticPagesController < ApplicationController
|
||||||
@todays_duration = current_user.heartbeats.today.duration_seconds
|
@todays_duration = current_user.heartbeats.today.duration_seconds
|
||||||
@show_logged_time_sentence = @todays_duration > 1.minute && (@todays_languages.any? || @todays_editors.any?)
|
@show_logged_time_sentence = @todays_duration > 1.minute && (@todays_languages.any? || @todays_editors.any?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
render inertia: "Home/SignedIn", props: signed_in_props
|
||||||
else
|
else
|
||||||
# Set homepage SEO content for logged-out users only
|
# Set homepage SEO content for logged-out users only
|
||||||
set_homepage_seo_content
|
set_homepage_seo_content
|
||||||
|
|
@ -53,6 +56,8 @@ class StaticPagesController < ApplicationController
|
||||||
@usage_social_proof = Cache::UsageSocialProofJob.perform_now
|
@usage_social_proof = Cache::UsageSocialProofJob.perform_now
|
||||||
|
|
||||||
@home_stats = Cache::HomeStatsJob.perform_now
|
@home_stats = Cache::HomeStatsJob.perform_now
|
||||||
|
|
||||||
|
render inertia: "Home/SignedOut", props: signed_out_props
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -67,12 +72,6 @@ class StaticPagesController < ApplicationController
|
||||||
@meta_keywords = "what is hackatime, hackatime definition, hack club time tracker, coding time tracker, programming statistics"
|
@meta_keywords = "what is hackatime, hackatime definition, hack club time tracker, coding time tracker, programming statistics"
|
||||||
end
|
end
|
||||||
|
|
||||||
def mini_leaderboard
|
|
||||||
@leaderboard = LeaderboardService.get(period: :daily, date: Date.current)
|
|
||||||
@active_projects = Cache::ActiveProjectsJob.perform_now
|
|
||||||
render partial: "leaderboards/mini_leaderboard", locals: { leaderboard: @leaderboard, current_user: current_user }
|
|
||||||
end
|
|
||||||
|
|
||||||
def project_durations
|
def project_durations
|
||||||
return unless current_user
|
return unless current_user
|
||||||
|
|
||||||
|
|
@ -107,18 +106,6 @@ class StaticPagesController < ApplicationController
|
||||||
render partial: "project_durations", locals: { project_durations: durations, show_archived: archived }
|
render partial: "project_durations", locals: { project_durations: durations, show_archived: archived }
|
||||||
end
|
end
|
||||||
|
|
||||||
def activity_graph
|
|
||||||
return unless current_user
|
|
||||||
|
|
||||||
tz = current_user.timezone
|
|
||||||
key = "user_#{current_user.id}_daily_durations_#{tz}"
|
|
||||||
durations = Rails.cache.fetch(key, expires_in: 1.minute) do
|
|
||||||
Time.use_zone(tz) { current_user.heartbeats.daily_durations(user_timezone: tz).to_h }
|
|
||||||
end
|
|
||||||
|
|
||||||
render partial: "activity_graph", locals: { daily_durations: durations, length_of_busiest_day: 8.hours.to_i, user_tz: tz }
|
|
||||||
end
|
|
||||||
|
|
||||||
def currently_hacking
|
def currently_hacking
|
||||||
data = Cache::CurrentlyHackingJob.perform_now
|
data = Cache::CurrentlyHackingJob.perform_now
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|
@ -178,9 +165,45 @@ class StaticPagesController < ApplicationController
|
||||||
@meta_keywords = "coding time tracker, programming stats, open source time tracker, hack club coding tracker, free time tracking, code statistics, high school programming, coding analytics"
|
@meta_keywords = "coding time tracker, programming stats, open source time tracker, hack club coding tracker, free time tracking, code statistics, high school programming, coding analytics"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def signed_in_props
|
||||||
|
helpers = ApplicationController.helpers
|
||||||
|
{
|
||||||
|
flavor_text: @flavor_text.to_s,
|
||||||
|
trust_level_red: current_user&.trust_level == "red",
|
||||||
|
show_wakatime_setup_notice: !!@show_wakatime_setup_notice,
|
||||||
|
ssp_message: @ssp_message,
|
||||||
|
ssp_users_recent: @ssp_users_recent || [],
|
||||||
|
ssp_users_size: @ssp_users_size || @ssp_users_recent&.size || 0,
|
||||||
|
github_uid_blank: current_user&.github_uid.blank?,
|
||||||
|
github_auth_path: github_auth_path,
|
||||||
|
wakatime_setup_path: my_wakatime_setup_path,
|
||||||
|
show_logged_time_sentence: !!@show_logged_time_sentence,
|
||||||
|
todays_duration_display: helpers.short_time_detailed(@todays_duration.to_i),
|
||||||
|
todays_languages: @todays_languages || [],
|
||||||
|
todays_editors: @todays_editors || [],
|
||||||
|
filterable_dashboard_data: filterable_dashboard_data,
|
||||||
|
activity_graph: activity_graph_data
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed_out_props
|
||||||
|
{
|
||||||
|
flavor_text: @flavor_text.to_s,
|
||||||
|
hca_auth_path: hca_auth_path,
|
||||||
|
slack_auth_path: slack_auth_path,
|
||||||
|
email_auth_path: email_auth_path,
|
||||||
|
sign_in_email: params[:sign_in_email].present?,
|
||||||
|
show_dev_tool: Rails.env.development?,
|
||||||
|
dev_magic_link: (Rails.env.development? ? session.delete(:dev_magic_link) : nil),
|
||||||
|
csrf_token: form_authenticity_token,
|
||||||
|
home_stats: @home_stats || {}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def filterable_dashboard_data
|
def filterable_dashboard_data
|
||||||
filters = %i[project language operating_system editor category]
|
filters = %i[project language operating_system editor category]
|
||||||
key = [ current_user ] + filters.map { |f| params[f] } + [ params[:interval], params[:from], params[:to] ]
|
interval = params[:interval]
|
||||||
|
key = [ current_user ] + filters.map { |f| params[f] } + [ interval.to_s, params[:from], params[:to] ]
|
||||||
hb = current_user.heartbeats
|
hb = current_user.heartbeats
|
||||||
h = ApplicationController.helpers
|
h = ApplicationController.helpers
|
||||||
|
|
||||||
|
|
@ -190,9 +213,9 @@ class StaticPagesController < ApplicationController
|
||||||
|
|
||||||
Time.use_zone(current_user.timezone) do
|
Time.use_zone(current_user.timezone) do
|
||||||
filters.each do |f|
|
filters.each do |f|
|
||||||
durations = current_user.heartbeats.group(f).duration_seconds
|
options = current_user.heartbeats.distinct.pluck(f).compact_blank
|
||||||
durations = durations.reject { |n, _| archived.include?(n) } if f == :project
|
options = options.reject { |n| archived.include?(n) } if f == :project
|
||||||
result[f] = durations.sort_by { |_, v| -v }.map(&:first).compact_blank.map { |k|
|
result[f] = options.map { |k|
|
||||||
f == :language ? k.categorize_language : (%i[operating_system editor].include?(f) ? k.capitalize : k)
|
f == :language ? k.categorize_language : (%i[operating_system editor].include?(f) ? k.capitalize : k)
|
||||||
}.uniq
|
}.uniq
|
||||||
|
|
||||||
|
|
@ -209,8 +232,7 @@ class StaticPagesController < ApplicationController
|
||||||
result["singular_#{f}"] = arr.length == 1
|
result["singular_#{f}"] = arr.length == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
hb = hb.filter_by_time_range(params[:interval], params[:from], params[:to])
|
hb = hb.filter_by_time_range(interval, params[:from], params[:to])
|
||||||
result[:filtered_heartbeats] = hb
|
|
||||||
result[:total_time] = hb.group(:project).duration_seconds.values.sum
|
result[:total_time] = hb.group(:project).duration_seconds.values.sum
|
||||||
result[:total_heartbeats] = hb.count
|
result[:total_heartbeats] = hb.count
|
||||||
|
|
||||||
|
|
@ -247,13 +269,35 @@ class StaticPagesController < ApplicationController
|
||||||
}.to_h
|
}.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
result[:weekly_project_stats] = (0..25).to_h do |w|
|
result[:weekly_project_stats] = (0..11).to_h do |w|
|
||||||
ws = w.weeks.ago.beginning_of_week
|
ws = w.weeks.ago.beginning_of_week
|
||||||
[ ws.to_date.iso8601, hb.where(time: ws.to_f..w.weeks.ago.end_of_week.to_f)
|
[ ws.to_date.iso8601, hb.where(time: ws.to_f..w.weeks.ago.end_of_week.to_f)
|
||||||
.group(:project).duration_seconds.reject { |p, _| archived.include?(p) } ]
|
.group(:project).duration_seconds.reject { |p, _| archived.include?(p) } ]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
result[:selected_interval] = interval.to_s
|
||||||
|
result[:selected_from] = params[:from].to_s
|
||||||
|
result[:selected_to] = params[:to].to_s
|
||||||
|
filters.each { |f| result["selected_#{f}"] = params[f]&.split(",") || [] }
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def activity_graph_data
|
||||||
|
tz = current_user.timezone
|
||||||
|
key = "user_#{current_user.id}_daily_durations_#{tz}"
|
||||||
|
durations = Rails.cache.fetch(key, expires_in: 1.minute) do
|
||||||
|
Time.use_zone(tz) { current_user.heartbeats.daily_durations(user_timezone: tz).to_h }
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
start_date: 365.days.ago.to_date.iso8601,
|
||||||
|
end_date: Time.current.to_date.iso8601,
|
||||||
|
duration_by_date: durations.transform_keys { |d| d.to_date.iso8601 }.transform_values(&:to_i),
|
||||||
|
busiest_day_seconds: 8.hours.to_i,
|
||||||
|
timezone_label: ActiveSupport::TimeZone[tz].to_s,
|
||||||
|
timezone_settings_path: "/my/settings#user_timezone"
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
class UsersController < ApplicationController
|
class UsersController < InertiaController
|
||||||
|
layout "inertia"
|
||||||
|
|
||||||
include ActionView::Helpers::NumberHelper
|
include ActionView::Helpers::NumberHelper
|
||||||
|
|
||||||
before_action :set_user
|
before_action :set_user
|
||||||
|
|
@ -52,25 +54,38 @@ class UsersController < ApplicationController
|
||||||
def wakatime_setup
|
def wakatime_setup
|
||||||
api_key = current_user&.api_keys&.last
|
api_key = current_user&.api_keys&.last
|
||||||
api_key ||= current_user.api_keys.create!(name: "Wakatime API Key")
|
api_key ||= current_user.api_keys.create!(name: "Wakatime API Key")
|
||||||
@current_user_api_key = api_key&.token
|
setup_os = detect_setup_os(request.user_agent)
|
||||||
|
|
||||||
|
render inertia: "WakatimeSetup/Index", props: {
|
||||||
|
current_user_api_key: api_key&.token,
|
||||||
|
setup_os: setup_os.to_s,
|
||||||
|
api_url: api_hackatime_v1_url,
|
||||||
|
heartbeat_check_url: api_v1_my_heartbeats_most_recent_path(source_type: "test_entry")
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def wakatime_setup_step_2
|
def wakatime_setup_step_2
|
||||||
|
render inertia: "WakatimeSetup/Step2", props: {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def wakatime_setup_step_3
|
def wakatime_setup_step_3
|
||||||
api_key = current_user&.api_keys&.last
|
api_key = current_user&.api_keys&.last
|
||||||
api_key ||= current_user.api_keys.create!(name: "Wakatime API Key")
|
api_key ||= current_user.api_keys.create!(name: "Wakatime API Key")
|
||||||
@current_user_api_key = api_key&.token
|
editor = params[:editor]
|
||||||
|
|
||||||
|
render inertia: "WakatimeSetup/Step3", props: {
|
||||||
|
current_user_api_key: api_key&.token,
|
||||||
|
editor: editor,
|
||||||
|
heartbeat_check_url: api_v1_my_heartbeats_most_recent_path
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def wakatime_setup_step_4
|
def wakatime_setup_step_4
|
||||||
@no_instruction_wording = [
|
render inertia: "WakatimeSetup/Step4", props: {
|
||||||
"There is no step 4, lol.",
|
dino_video_url: FlavorText.dino_meme_videos.sample,
|
||||||
"There is no step 4, psych!",
|
return_url: session.dig(:return_data, "url"),
|
||||||
"Tricked ya! There is no step 4.",
|
return_button_text: session.dig(:return_data, "button_text") || "Done"
|
||||||
"There is no step 4, gotcha!"
|
}
|
||||||
].sample
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_trust_level
|
def update_trust_level
|
||||||
|
|
@ -125,6 +140,14 @@ class UsersController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def detect_setup_os(user_agent)
|
||||||
|
ua = user_agent.to_s
|
||||||
|
|
||||||
|
return :windows if ua.match?(/windows/i)
|
||||||
|
|
||||||
|
:mac_linux
|
||||||
|
end
|
||||||
|
|
||||||
def prepare_settings_page
|
def prepare_settings_page
|
||||||
@is_own_settings = is_own_settings?
|
@is_own_settings = is_own_settings?
|
||||||
@can_enable_slack_status = @user.slack_access_token.present? && @user.slack_scopes.include?("users.profile:write")
|
@can_enable_slack_status = @user.slack_access_token.present? && @user.slack_scopes.include?("users.profile:write")
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ function setupCurrentlyHacking() {
|
||||||
const header = document.querySelector('.currently-hacking');
|
const header = document.querySelector('.currently-hacking');
|
||||||
// only if no existing event listener
|
// only if no existing event listener
|
||||||
if (!header) { return }
|
if (!header) { return }
|
||||||
header.onclick = function() {
|
header.onclick = function () {
|
||||||
const container = document.querySelector('.currently-hacking-container');
|
const container = document.querySelector('.currently-hacking-container');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.classList.toggle('visible');
|
container.classList.toggle('visible');
|
||||||
|
|
@ -19,78 +19,21 @@ function outta() {
|
||||||
const modal = document.getElementById('logout-modal');
|
const modal = document.getElementById('logout-modal');
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
|
|
||||||
window.showLogout = function() {
|
window.showLogout = function () {
|
||||||
modal.dispatchEvent(new CustomEvent('modal:open', { bubbles: true }));
|
modal.dispatchEvent(new CustomEvent('modal:open', { bubbles: true }));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function weirdclockthing() {
|
|
||||||
const clock = document.getElementById('clock');
|
|
||||||
|
|
||||||
if (!clock) return;
|
|
||||||
|
|
||||||
clock.innerHTML = '';
|
|
||||||
|
|
||||||
function write(element, something) {
|
|
||||||
element.innerHTML = '';
|
|
||||||
Array.from(something).forEach((char) => {
|
|
||||||
const span = document.createElement('span');
|
|
||||||
span.textContent = char === ' ' ? '\u00A0' : char;
|
|
||||||
if (char === ':') {
|
|
||||||
span.classList.add('blink');
|
|
||||||
}
|
|
||||||
element.appendChild(span);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const inner = document.createElement('div');
|
|
||||||
inner.className = 'clock-display-inner';
|
|
||||||
|
|
||||||
const front = document.createElement('div');
|
|
||||||
// kinda janky lol
|
|
||||||
front.className = 'clock-display-front ds-digital';
|
|
||||||
write(front, "HAC:KA:TIME");
|
|
||||||
|
|
||||||
const back = document.createElement('div');
|
|
||||||
back.className = 'clock-display-back';
|
|
||||||
|
|
||||||
inner.appendChild(front);
|
|
||||||
inner.appendChild(back);
|
|
||||||
clock.appendChild(inner);
|
|
||||||
|
|
||||||
function updateClock() {
|
|
||||||
const date = new Date();
|
|
||||||
const time = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
|
|
||||||
write(back, ` ${time} `);
|
|
||||||
}
|
|
||||||
|
|
||||||
let intervalId = null;
|
|
||||||
clock.onmouseenter = function () {
|
|
||||||
updateClock();
|
|
||||||
if (!intervalId) {
|
|
||||||
intervalId = setInterval(updateClock, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clock.onmouseleave = function () {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
intervalId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle both initial page load and subsequent Turbo navigations
|
// Handle both initial page load and subsequent Turbo navigations
|
||||||
document.addEventListener('turbo:load', function() {
|
document.addEventListener('turbo:load', function () {
|
||||||
setupCurrentlyHacking();
|
setupCurrentlyHacking();
|
||||||
outta();
|
outta();
|
||||||
weirdclockthing();
|
|
||||||
});
|
});
|
||||||
document.addEventListener('turbo:render', function() {
|
document.addEventListener('turbo:render', function () {
|
||||||
setupCurrentlyHacking();
|
setupCurrentlyHacking();
|
||||||
outta();
|
outta();
|
||||||
weirdclockthing();
|
|
||||||
});
|
});
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
setupCurrentlyHacking();
|
setupCurrentlyHacking();
|
||||||
outta();
|
outta();
|
||||||
weirdclockthing();
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export default class extends Controller {
|
||||||
|
|
||||||
showLoading() {
|
showLoading() {
|
||||||
this.contentTarget.innerHTML = `
|
this.contentTarget.innerHTML = `
|
||||||
<div class="p-4">
|
<div class="p-4 bg-dark">
|
||||||
<div class="text-center text-muted text-md">Loading...</div>
|
<div class="text-center text-muted text-md">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
@ -113,7 +113,7 @@ export default class extends Controller {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to poll currently hacking:", error)
|
console.error("Failed to poll currently hacking:", error)
|
||||||
this.contentTarget.innerHTML = `
|
this.contentTarget.innerHTML = `
|
||||||
<div class="p-4 bg-elevated">
|
<div class="p-4 bg-dark">
|
||||||
<div class="text-center text-muted text-sm">ruh ro, something broke :(</div>
|
<div class="text-center text-muted text-sm">ruh ro, something broke :(</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
@ -132,7 +132,7 @@ export default class extends Controller {
|
||||||
r(u) {
|
r(u) {
|
||||||
if (!u || u.length === 0) {
|
if (!u || u.length === 0) {
|
||||||
this.contentTarget.innerHTML = `
|
this.contentTarget.innerHTML = `
|
||||||
<div class="p-4 bg-elevated">
|
<div class="p-4 bg-dark">
|
||||||
<div class="text-center text-muted text-sm italic">No one is currently hacking :(</div>
|
<div class="text-center text-muted text-sm italic">No one is currently hacking :(</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
|
||||||
4
app/javascript/entrypoints/application.css
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@plugin '@tailwindcss/typography';
|
||||||
|
@plugin '@tailwindcss/forms';
|
||||||
28
app/javascript/entrypoints/application.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
// To see this message, add the following to the `<head>` section in your
|
||||||
|
// views/layouts/application.html.erb
|
||||||
|
//
|
||||||
|
// <%= vite_client_tag %>
|
||||||
|
// <%= vite_javascript_tag 'application' %>
|
||||||
|
console.log('Vite ⚡️ Rails')
|
||||||
|
|
||||||
|
// If using a TypeScript entrypoint file:
|
||||||
|
// <%= vite_typescript_tag 'application' %>
|
||||||
|
//
|
||||||
|
// If you want to use .jsx or .tsx, add the extension:
|
||||||
|
// <%= vite_javascript_tag 'application.jsx' %>
|
||||||
|
|
||||||
|
console.log('Visit the guide for more information: ', 'https://vite-ruby.netlify.app/guide/rails')
|
||||||
|
|
||||||
|
// Example: Load Rails libraries in Vite.
|
||||||
|
//
|
||||||
|
// import * as Turbo from '@hotwired/turbo'
|
||||||
|
// Turbo.start()
|
||||||
|
//
|
||||||
|
// import ActiveStorage from '@rails/activestorage'
|
||||||
|
// ActiveStorage.start()
|
||||||
|
//
|
||||||
|
// // Import all channels.
|
||||||
|
// const channels = import.meta.glob('./**/*_channel.js', { eager: true })
|
||||||
|
|
||||||
|
// Example: Import a stylesheet in app/frontend/index.css
|
||||||
|
// import '~/index.css'
|
||||||
46
app/javascript/entrypoints/inertia.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { createInertiaApp, type ResolvedComponent } from '@inertiajs/svelte'
|
||||||
|
import { mount } from 'svelte'
|
||||||
|
import AppLayout from '../layouts/AppLayout.svelte'
|
||||||
|
|
||||||
|
createInertiaApp({
|
||||||
|
// Disable progress bar
|
||||||
|
//
|
||||||
|
// see https://inertia-rails.dev/guide/progress-indicators
|
||||||
|
// progress: false,
|
||||||
|
|
||||||
|
resolve: (name) => {
|
||||||
|
const pages = import.meta.glob<ResolvedComponent>('../pages/**/*.svelte', {
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
const page = pages[`../pages/${name}.svelte`]
|
||||||
|
if (!page) {
|
||||||
|
console.error(`Missing Inertia page component: '${name}.svelte'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { default: page.default, layout: page.layout || AppLayout } as ResolvedComponent
|
||||||
|
},
|
||||||
|
|
||||||
|
setup({ el, App, props }) {
|
||||||
|
if (el) {
|
||||||
|
mount(App, { target: el, props })
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
'Missing root element.\n\n' +
|
||||||
|
'If you see this error, it probably means you load Inertia.js on non-Inertia pages.\n' +
|
||||||
|
'Consider moving <%= vite_typescript_tag "inertia" %> to the Inertia-specific layout instead.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
form: {
|
||||||
|
forceIndicesArrayFormatInFormData: false,
|
||||||
|
},
|
||||||
|
future: {
|
||||||
|
useScriptElementForInitialPage: true,
|
||||||
|
useDataInertiaHeadAttribute: true,
|
||||||
|
useDialogForErrorModal: true,
|
||||||
|
preserveEqualProps: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
5
app/javascript/inertia_app.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import "@hotwired/turbo-rails"
|
||||||
|
import "@fontsource/inter/400.css"
|
||||||
|
import "@fontsource/inter/500.css"
|
||||||
|
import "@fontsource/inter/600.css"
|
||||||
|
import "@fontsource/inter/700.css"
|
||||||
505
app/javascript/layouts/AppLayout.svelte
Normal file
|
|
@ -0,0 +1,505 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { router } from "@inertiajs/svelte";
|
||||||
|
import { usePoll } from "@inertiajs/svelte";
|
||||||
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import plur from "plur";
|
||||||
|
|
||||||
|
type NavLink = {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
active?: boolean;
|
||||||
|
badge?: number | null;
|
||||||
|
action?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LayoutNav = {
|
||||||
|
flash: { message: string; class_name: string }[];
|
||||||
|
user_present: boolean;
|
||||||
|
user_mention_html?: string | null;
|
||||||
|
streak_html?: string | null;
|
||||||
|
admin_level_html?: string | null;
|
||||||
|
login_path: string;
|
||||||
|
links: NavLink[];
|
||||||
|
dev_links: NavLink[];
|
||||||
|
admin_links: NavLink[];
|
||||||
|
viewer_links: NavLink[];
|
||||||
|
superadmin_links: NavLink[];
|
||||||
|
activities_html?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Footer = {
|
||||||
|
git_version: string;
|
||||||
|
commit_link: string;
|
||||||
|
server_start_time_ago: string;
|
||||||
|
heartbeat_recent_count: number;
|
||||||
|
heartbeat_recent_imported_count: number;
|
||||||
|
query_count: number;
|
||||||
|
query_cache_count: number;
|
||||||
|
cache_hits: number;
|
||||||
|
cache_misses: number;
|
||||||
|
requests_per_second: string;
|
||||||
|
active_users_graph: { height: number; title: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CurrentlyHackingUser = {
|
||||||
|
id: number;
|
||||||
|
display_name?: string;
|
||||||
|
slack_uid?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
active_project?: { name: string; repo_url?: string | null };
|
||||||
|
};
|
||||||
|
|
||||||
|
type LayoutProps = {
|
||||||
|
nav: LayoutNav;
|
||||||
|
footer: Footer;
|
||||||
|
currently_hacking: {
|
||||||
|
count: number;
|
||||||
|
users: CurrentlyHackingUser[];
|
||||||
|
interval: number;
|
||||||
|
};
|
||||||
|
csrf_token: string;
|
||||||
|
signout_path: string;
|
||||||
|
show_stop_impersonating: boolean;
|
||||||
|
stop_impersonating_path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { layout, children }: { layout: LayoutProps; children?: () => unknown } =
|
||||||
|
$props();
|
||||||
|
|
||||||
|
const isBrowser =
|
||||||
|
typeof window !== "undefined" && typeof document !== "undefined";
|
||||||
|
|
||||||
|
let navOpen = $state(false);
|
||||||
|
let logoutOpen = $state(false);
|
||||||
|
let currentlyExpanded = $state(false);
|
||||||
|
|
||||||
|
const toggleNav = () => (navOpen = !navOpen);
|
||||||
|
const closeNav = () => (navOpen = false);
|
||||||
|
const openLogout = () => (logoutOpen = true);
|
||||||
|
const closeLogout = () => (logoutOpen = false);
|
||||||
|
|
||||||
|
usePoll(layout.currently_hacking?.interval || 30000, {
|
||||||
|
only: ["currently_hacking"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleNavLinkClick = () => {
|
||||||
|
if (isBrowser && window.innerWidth <= 1024) closeNav();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (isBrowser && window.innerWidth > 1024) closeNav();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeNav();
|
||||||
|
closeLogout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const countLabel = () =>
|
||||||
|
`${layout.currently_hacking.count} ${plur("person", layout.currently_hacking.count)} currently hacking`;
|
||||||
|
|
||||||
|
const visualizeGitUrl = (url?: string | null) =>
|
||||||
|
url?.startsWith("https://github.com/")
|
||||||
|
? url.replace(
|
||||||
|
"https://github.com/",
|
||||||
|
"https://tkww0gcc0gkwwo4gc8kgs0sw.a.selfhosted.hackclub.com/",
|
||||||
|
)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const toggleCurrentlyHacking = () => {
|
||||||
|
currentlyExpanded = !currentlyExpanded;
|
||||||
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isBrowser) document.body.classList.toggle("overflow-hidden", navOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!isBrowser) return;
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
document.addEventListener("keydown", handleKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (isBrowser) {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
document.removeEventListener("keydown", handleKeydown);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const navLinkClass = (active?: boolean) =>
|
||||||
|
`block px-3 py-2 rounded-md text-sm transition-colors ${active ? "bg-primary text-white" : "hover:bg-darkless"}`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if layout.nav.flash.length > 0}
|
||||||
|
<div class="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 w-full max-w-md px-4 space-y-2">
|
||||||
|
{#each layout.nav.flash as item}
|
||||||
|
<div
|
||||||
|
class={`rounded-md text-center text-sm px-4 py-3 shadow-lg ${item.class_name}`}
|
||||||
|
>
|
||||||
|
{item.message}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if layout.nav.user_present}
|
||||||
|
<button
|
||||||
|
class="mobile-nav-button"
|
||||||
|
aria-label="Toggle navigation menu"
|
||||||
|
aria-expanded={navOpen}
|
||||||
|
onclick={toggleNav}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="nav-overlay" class:open={navOpen} onclick={closeNav}></div>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
class="flex flex-col min-h-screen w-52 bg-dark text-white px-3 py-4 rounded-r-lg overflow-y-auto lg:block"
|
||||||
|
data-nav-target="nav"
|
||||||
|
class:open={navOpen}
|
||||||
|
style="scrollbar-width: none; -ms-overflow-style: none;"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
{#if layout.nav.user_present}
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center gap-2 pb-3 border-b border-darkless"
|
||||||
|
>
|
||||||
|
{#if layout.nav.user_mention_html}{@html layout.nav
|
||||||
|
.user_mention_html}{/if}
|
||||||
|
{#if layout.nav.streak_html}{@html layout.nav.streak_html}{/if}
|
||||||
|
{#if layout.nav.admin_level_html}{@html layout.nav
|
||||||
|
.admin_level_html}{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href={layout.nav.login_path}
|
||||||
|
class="block px-4 py-2 rounded-md transition text-white font-semibold bg-primary hover:bg-secondary text-center"
|
||||||
|
>Login</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<nav class="space-y-1">
|
||||||
|
{#each layout.nav.links as link}
|
||||||
|
{#if link.action === "logout"}
|
||||||
|
<a
|
||||||
|
type="button"
|
||||||
|
onclick={openLogout}
|
||||||
|
class={`${navLinkClass(false)} cursor-pointer`}>Logout</a
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
onclick={handleNavLinkClick}
|
||||||
|
class={navLinkClass(link.active)}>{link.label}</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if layout.nav.dev_links.length > 0 || layout.nav.admin_links.length > 0 || layout.nav.viewer_links.length > 0 || layout.nav.superadmin_links.length > 0}
|
||||||
|
<div class="pt-2 mt-2 border-t border-darkless space-y-1">
|
||||||
|
{#each layout.nav.dev_links as link}
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
onclick={handleNavLinkClick}
|
||||||
|
class="{navLinkClass(link.active)} dev-tool"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
{#if link.badge}
|
||||||
|
<span
|
||||||
|
class="ml-1 px-1.5 py-0.5 text-xs rounded-full bg-primary text-white font-medium"
|
||||||
|
>
|
||||||
|
{link.badge}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each layout.nav.admin_links as link}
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
onclick={handleNavLinkClick}
|
||||||
|
class="{navLinkClass(link.active)} admin-tool"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
{#if link.badge}
|
||||||
|
<span
|
||||||
|
class="ml-1 px-1.5 py-0.5 text-xs rounded-full bg-primary text-white font-medium"
|
||||||
|
>
|
||||||
|
{link.badge}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each layout.nav.viewer_links as link}
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
onclick={handleNavLinkClick}
|
||||||
|
class="{navLinkClass(link.active)} viewer-tool"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
{#if link.badge}
|
||||||
|
<span
|
||||||
|
class="ml-1 px-1.5 py-0.5 text-xs rounded-full bg-primary text-white font-medium"
|
||||||
|
>
|
||||||
|
{link.badge}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each layout.nav.superadmin_links as link}
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
onclick={handleNavLinkClick}
|
||||||
|
class="{navLinkClass(link.active)} superadmin-tool"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
{#if link.badge}
|
||||||
|
<span
|
||||||
|
class="ml-1 px-1.5 py-0.5 text-xs rounded-full bg-primary text-white font-medium"
|
||||||
|
>
|
||||||
|
{link.badge}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if layout.nav.activities_html}
|
||||||
|
<div class="pt-2">{@html layout.nav.activities_html}</div>
|
||||||
|
{/if}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<main
|
||||||
|
class={`flex-1 min-h-screen transition-all duration-300 ease-in-out ${layout.nav.user_present ? "lg:ml-62.5" : ""}`}
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-7xl mx-auto p-4 pt-16 lg:pt-8 md:p-8">
|
||||||
|
{@render children?.()}
|
||||||
|
|
||||||
|
<footer
|
||||||
|
class="relative w-full mt-12 mb-5 p-2.5 text-center text-xs text-text-muted"
|
||||||
|
>
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<p
|
||||||
|
class="brightness-60 hover:brightness-100 transition-all duration-200"
|
||||||
|
>
|
||||||
|
Using Inertia. Build <a
|
||||||
|
href={layout.footer.commit_link}
|
||||||
|
class="text-inherit underline opacity-80 hover:opacity-100 transition-opacity duration-200"
|
||||||
|
>{layout.footer.git_version}</a
|
||||||
|
>
|
||||||
|
from {layout.footer.server_start_time_ago} ago.
|
||||||
|
{plur("heartbeat", layout.footer.heartbeat_recent_count)}
|
||||||
|
({layout.footer.heartbeat_recent_imported_count} imported) in the past
|
||||||
|
24 hours. (DB: {layout.footer.query_count}
|
||||||
|
{plur("query", layout.footer.query_count)}, {layout.footer
|
||||||
|
.query_cache_count} cached) (CACHE: {layout.footer.cache_hits} hits,
|
||||||
|
{layout.footer.cache_misses} misses) ({layout.footer
|
||||||
|
.requests_per_second})
|
||||||
|
</p>
|
||||||
|
{#if layout.show_stop_impersonating}
|
||||||
|
<a
|
||||||
|
href={layout.stop_impersonating_path}
|
||||||
|
data-turbo-prefetch="false"
|
||||||
|
class="text-primary font-bold hover:text-red-300 transition-colors duration-200"
|
||||||
|
>Stop impersonating</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2 mt-4 justify-center">
|
||||||
|
{#each layout.footer.active_users_graph as hour}
|
||||||
|
<div
|
||||||
|
class="bg-white opacity-10 grow max-w-1 rounded-sm"
|
||||||
|
title={hour.title}
|
||||||
|
style={`height: ${hour.height}px`}
|
||||||
|
></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{#if layout.currently_hacking}
|
||||||
|
<div
|
||||||
|
class="fixed top-0 right-5 max-w-sm max-h-[80vh] bg-dark border border-darkless rounded-b-xl shadow-lg z-1000 overflow-hidden transform transition-transform duration-300 ease-out"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="currently-hacking p-3 bg-dark cursor-pointer select-none flex items-center justify-between"
|
||||||
|
onclick={toggleCurrentlyHacking}
|
||||||
|
>
|
||||||
|
<div class="text-white text-sm font-medium">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full bg-green-500 animate-pulse mr-2"
|
||||||
|
></div>
|
||||||
|
<span class="text-base">{countLabel()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if currentlyExpanded}
|
||||||
|
{#if layout.currently_hacking.users.length === 0}
|
||||||
|
<div class="p-4 bg-dark">
|
||||||
|
<div class="text-center text-muted text-sm italic">
|
||||||
|
No one is currently hacking :(
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="currently-hacking-list max-h-[60vh] max-w-100 overflow-y-auto p-2 bg-darker"
|
||||||
|
>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each layout.currently_hacking.users as user}
|
||||||
|
<div
|
||||||
|
class="flex flex-col space-y-1 p-2 rounded-md hover:bg-dark transition-colors"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{#if user.avatar_url}
|
||||||
|
<img
|
||||||
|
src={user.avatar_url}
|
||||||
|
alt={`${user.display_name || `User ${user.id}`}'s avatar`}
|
||||||
|
class="w-6 h-6 rounded-full aspect-square flex-shrink-0"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if user.slack_uid}
|
||||||
|
<a
|
||||||
|
href={`https://hackclub.slack.com/team/${user.slack_uid}`}
|
||||||
|
target="_blank"
|
||||||
|
class="text-blue-500 hover:underline text-sm"
|
||||||
|
>
|
||||||
|
@{user.display_name || `User ${user.id}`}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<span class="text-white text-sm"
|
||||||
|
>{user.display_name || `User ${user.id}`}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if user.active_project}
|
||||||
|
<div class="text-xs text-muted ml-8">
|
||||||
|
working on
|
||||||
|
{#if user.active_project.repo_url}
|
||||||
|
<a
|
||||||
|
href={user.active_project.repo_url}
|
||||||
|
target="_blank"
|
||||||
|
class="text-accent hover:text-cyan-400 transition-colors"
|
||||||
|
>
|
||||||
|
{user.active_project.name}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
{user.active_project.name}
|
||||||
|
{/if}
|
||||||
|
{#if visualizeGitUrl(user.active_project.repo_url)}
|
||||||
|
<a
|
||||||
|
href={visualizeGitUrl(user.active_project.repo_url)}
|
||||||
|
target="_blank"
|
||||||
|
class="ml-1">🌌</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 flex items-center justify-center z-9999 transition-opacity duration-300 ease-in-out"
|
||||||
|
class:opacity-0={!logoutOpen}
|
||||||
|
class:pointer-events-none={!logoutOpen}
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.5);backdrop-filter: blur(4px);"
|
||||||
|
onclick={(e) => e.target === e.currentTarget && closeLogout()}
|
||||||
|
>
|
||||||
|
<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"}`}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center w-full">
|
||||||
|
<div class="mb-4 flex justify-center w-full">
|
||||||
|
<svg
|
||||||
|
class="w-12 h-12 text-primary"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M5 21q-.825 0-1.412-.587T3 19v-3q0-.425.288-.712T4 15t.713.288T5 16v3h14V5H5v3q0 .425-.288.713T4 9t-.712-.288T3 8V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm6.65-8H4q-.425 0-.712-.288T3 12t.288-.712T4 11h7.65L9.8 9.15q-.3-.3-.288-.7t.288-.7q.3-.3.713-.312t.712.287L14.8 11.3q.15.15.213.325t.062.375t-.062.375t-.213.325l-3.575 3.575q-.3.3-.712.288T9.8 16.25q-.275-.3-.288-.7t.288-.7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-2xl font-bold text-white mb-2 text-center w-full">
|
||||||
|
Woah hold on a sec
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-300 mb-6 text-center w-full">
|
||||||
|
You sure you want to log out? You can sign back in later but that is a
|
||||||
|
bit of a hassle...
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex w-full gap-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={closeLogout}
|
||||||
|
class="w-full h-10 px-4 rounded-lg transition-colors duration-200 cursor-pointer m-0 bg-dark hover:bg-darkless border border-darkless text-gray-300"
|
||||||
|
>Go back</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<form method="post" action={layout.signout_path} class="m-0">
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="authenticity_token"
|
||||||
|
value={layout.csrf_token}
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="_method" value="delete" />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full h-10 px-4 rounded-lg transition-colors duration-200 font-medium cursor-pointer m-0 bg-primary hover:bg-primary/75 text-white"
|
||||||
|
>Log out now</button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(#app) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
136
app/javascript/pages/Docs/Index.svelte
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
popular_editors,
|
||||||
|
all_editors,
|
||||||
|
}: {
|
||||||
|
popular_editors: [string, string][];
|
||||||
|
all_editors: [string, string][];
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Documentation - Hackatime</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Complete documentation for Hackatime - learn how to track your coding time and use our API."
|
||||||
|
/>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-surface-content mb-2">Documentation</h1>
|
||||||
|
<p class="text-muted">
|
||||||
|
Free, open-source time tracking for Hack Club. Like WakaTime, but free.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||||
|
<a
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div class="text-2xl mb-2">⚡</div>
|
||||||
|
<h3 class="font-semibold text-surface-content">Quick Start</h3>
|
||||||
|
<p class="text-sm text-muted text-center mt-1">
|
||||||
|
Set up in under a minute
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div class="text-2xl mb-2">💻</div>
|
||||||
|
<h3 class="font-semibold text-surface-content">Installation</h3>
|
||||||
|
<p class="text-sm text-muted text-center mt-1">Add to your editor</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<div class="text-2xl mb-2">📡</div>
|
||||||
|
<h3 class="font-semibold text-surface-content">API Docs</h3>
|
||||||
|
<p class="text-sm text-muted text-center mt-1">Interactive reference</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold text-surface-content mb-4">
|
||||||
|
Popular Editors
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-3 mb-8">
|
||||||
|
{#each popular_editors as [name, slug]}
|
||||||
|
<a
|
||||||
|
href={`/docs/editors/${slug}`}
|
||||||
|
class="flex flex-col items-center p-3 bg-surface border border-surface-200 rounded-lg hover:border-primary transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`/images/editor-icons/${slug}-128.png`}
|
||||||
|
alt={name}
|
||||||
|
class="w-10 h-10 mb-2"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-surface-content">{name}</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="group">
|
||||||
|
<summary
|
||||||
|
class="flex items-center justify-between p-4 bg-surface border border-surface-200 rounded-lg cursor-pointer hover:border-surface-300 transition-colors select-none"
|
||||||
|
>
|
||||||
|
<span class="font-medium text-surface-content"
|
||||||
|
>All {all_editors.length} supported editors</span
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-muted group-open:rotate-180 transition-transform"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2 mt-3 p-4 bg-surface border border-surface-200 rounded-lg select-none"
|
||||||
|
>
|
||||||
|
{#each all_editors as [name, slug]}
|
||||||
|
<a
|
||||||
|
href={`/docs/editors/${slug}`}
|
||||||
|
class="flex flex-col items-center p-2 rounded hover:bg-surface-200 transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`/images/editor-icons/${slug}-128.png`}
|
||||||
|
alt={name}
|
||||||
|
class="w-8 h-8 mb-1"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-surface-content text-center leading-tight"
|
||||||
|
>{name}</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="mt-8 p-4 bg-surface border border-surface-200 rounded-lg">
|
||||||
|
<p class="text-sm text-muted">
|
||||||
|
Need help? Ask in
|
||||||
|
<a
|
||||||
|
href="https://hackclub.slack.com/archives/C07MQ845X1F"
|
||||||
|
target="_blank"
|
||||||
|
class="text-primary hover:underline">#hackatime-v2</a
|
||||||
|
>
|
||||||
|
on Slack or
|
||||||
|
<a
|
||||||
|
href="https://github.com/hackclub/hackatime/issues"
|
||||||
|
target="_blank"
|
||||||
|
class="text-primary hover:underline">open an issue</a
|
||||||
|
>
|
||||||
|
on GitHub.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
119
app/javascript/pages/Docs/Show.svelte
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { inertia } from "@inertiajs/svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
doc_path,
|
||||||
|
title,
|
||||||
|
rendered_content,
|
||||||
|
breadcrumbs,
|
||||||
|
edit_url,
|
||||||
|
meta,
|
||||||
|
}: {
|
||||||
|
doc_path: string;
|
||||||
|
title: string;
|
||||||
|
rendered_content: string;
|
||||||
|
breadcrumbs: { name: string; href: string | null; is_link: boolean }[];
|
||||||
|
edit_url: string;
|
||||||
|
meta: { description: string; keywords: string };
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{title} - Hackatime Documentation</title>
|
||||||
|
<meta name="description" content={meta.description} />
|
||||||
|
<meta name="keywords" content={meta.keywords} />
|
||||||
|
<meta property="og:title" content={`${title} - Hackatime Documentation`} />
|
||||||
|
<meta property="og:description" content={meta.description} />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="min-h-screen text-white">
|
||||||
|
<div class="max-w-8xl md:max-w-6xl mx-auto px-6 py-8">
|
||||||
|
<!-- Breadcrumbs -->
|
||||||
|
<nav class="mb-8">
|
||||||
|
{#each breadcrumbs as crumb, index}
|
||||||
|
{#if index === breadcrumbs.length - 1}
|
||||||
|
<span class="text-primary">{crumb.name}</span>
|
||||||
|
{:else}
|
||||||
|
{#if crumb.is_link && crumb.href}
|
||||||
|
<a
|
||||||
|
href={crumb.href}
|
||||||
|
use:inertia
|
||||||
|
class="text-secondary hover:text-primary">{crumb.name}</a
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<span class="text-secondary">{crumb.name}</span>
|
||||||
|
{/if}
|
||||||
|
<span class="text-secondary mx-2">/</span>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div
|
||||||
|
class="bg-dark rounded-lg p-8 mb-8 prose prose-invert prose-lg max-w-none
|
||||||
|
prose-headings:text-primary prose-headings:font-bold prose-headings:leading-tight
|
||||||
|
prose-h1:text-4xl prose-h1:mb-6 prose-h1:text-primary prose-h1:mt-0
|
||||||
|
prose-h2:text-2xl prose-h2:mt-10 prose-h2:mb-4 prose-h2:text-primary prose-h2:border-b prose-h2:border-b-[#6e6468] prose-h2:pb-2
|
||||||
|
prose-h3:text-xl prose-h3:mt-8 prose-h3:mb-3 prose-h3:text-primary
|
||||||
|
prose-h4:text-lg prose-h4:mt-4 prose-h4:mb-2 prose-h4:text-white prose-h4:font-semibold
|
||||||
|
prose-p:text-white prose-p:leading-7 prose-p:mb-5
|
||||||
|
prose-a:text-primary prose-a:hover:text-red prose-a:underline prose-a:font-medium
|
||||||
|
prose-strong:text-white prose-strong:font-semibold
|
||||||
|
prose-em:text-secondary prose-em:italic
|
||||||
|
prose-code:bg-darkless prose-code:text-primary prose-code:px-2 prose-code:py-1 prose-code:rounded prose-code:text-sm prose-code:font-mono
|
||||||
|
prose-pre:bg-darkless prose-pre:border prose-pre:border-primary/20 prose-pre:rounded-lg prose-pre:p-4 prose-pre:overflow-x-auto
|
||||||
|
prose-pre:text-white prose-pre:text-sm
|
||||||
|
prose-blockquote:border-l-4 prose-blockquote:border-primary prose-blockquote:bg-darkless prose-blockquote:pl-6 prose-blockquote:py-4 prose-blockquote:rounded-r-lg
|
||||||
|
prose-blockquote:text-secondary prose-blockquote:italic prose-blockquote:font-normal prose-blockquote:my-6
|
||||||
|
prose-ul:text-white prose-ul:mb-4 prose-ul:pl-6
|
||||||
|
prose-ol:text-white prose-ol:mb-4 prose-ol:pl-6
|
||||||
|
prose-li:text-white prose-li:mb-3 prose-li:leading-7 prose-li:pl-2
|
||||||
|
prose-table:border-collapse prose-table:border prose-table:border-primary/20 prose-table:rounded-lg prose-table:overflow-hidden prose-table:my-6
|
||||||
|
prose-th:bg-darkless prose-th:text-primary prose-th:font-semibold prose-th:p-3 prose-th:border prose-th:border-primary/20
|
||||||
|
prose-td:text-white prose-td:p-3 prose-td:border prose-td:border-primary/20
|
||||||
|
prose-img:rounded-lg prose-img:shadow-lg prose-img:mx-auto prose-img:block prose-img:max-w-24 prose-img:h-auto prose-img:my-4
|
||||||
|
prose-hr:border-primary/30 prose-hr:my-8
|
||||||
|
[&_ol>li::marker]:text-primary [&_ol>li::marker]:font-semibold
|
||||||
|
[&_ul>li::marker]:text-primary
|
||||||
|
[&_ol>li]:mb-3 [&_ol>li]:pl-2
|
||||||
|
[&_h2:not(:first-child)]:mt-10
|
||||||
|
[&_h3:not(:first-child)]:mt-8
|
||||||
|
[&_p_strong:first-child]:text-primary
|
||||||
|
[&_pre[class*='language-json']]:bg-darkless [&_pre[class*='language-json']]:border [&_pre[class*='language-json']]:border-primary/10
|
||||||
|
[&_pre[class*='language-bash']]:bg-darkless [&_pre[class*='language-bash']]:border [&_pre[class*='language-bash']]:border-primary/10
|
||||||
|
[&_img[alt*='PyCharm']]:w-16 [&_img[alt*='PyCharm']]:h-16 [&_img[alt*='PyCharm']]:mx-auto [&_img[alt*='PyCharm']]:block [&_img[alt*='PyCharm']]:my-4
|
||||||
|
[&_img[alt*='VS_Code']]:w-16 [&_img[alt*='VS_Code']]:h-16 [&_img[alt*='VS_Code']]:mx-auto [&_img[alt*='VS_Code']]:block [&_img[alt*='VS_Code']]:my-4
|
||||||
|
[&_img[alt*='IntelliJ']]:w-16 [&_img[alt*='IntelliJ']]:h-16 [&_img[alt*='IntelliJ']]:mx-auto [&_img[alt*='IntelliJ']]:block [&_img[alt*='IntelliJ']]:my-4
|
||||||
|
[&_img[src*='/images/editor-icons/']]:w-16 [&_img[src*='/images/editor-icons/']]:h-16 [&_img[src*='/images/editor-icons/']]:mx-auto [&_img[src*='/images/editor-icons/']]:block [&_img[src*='/images/editor-icons/']]:my-4
|
||||||
|
[&_.editor-steps]:bg-darkless [&_.editor-steps]:p-6 [&_.editor-steps]:rounded-lg [&_.editor-steps]:my-4
|
||||||
|
[&_.editor-steps_ol]:m-0
|
||||||
|
[&_.editor-steps_li]:mb-2"
|
||||||
|
>
|
||||||
|
{@html rendered_content}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit on GitHub -->
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center gap-2 py-6 text-sm text-secondary/70"
|
||||||
|
>
|
||||||
|
<span>Found an issue with this page?</span>
|
||||||
|
<a
|
||||||
|
href={edit_url}
|
||||||
|
target="_blank"
|
||||||
|
class="inline-flex items-center gap-1 text-primary hover:text-red transition-colors font-medium"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-4 h-4"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
><path
|
||||||
|
d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
Edit on GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
31
app/javascript/pages/Errors/NotFound.svelte
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
status_code = 404,
|
||||||
|
title = "Page Not Found",
|
||||||
|
message = "The page you were looking for doesn't exist.",
|
||||||
|
}: {
|
||||||
|
status_code?: number;
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{title} - Hackatime</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="min-h-screen text-white flex items-center justify-center">
|
||||||
|
<div class="max-w-xl mx-auto px-6 py-8 text-center">
|
||||||
|
<div class="bg-dark rounded-lg p-8">
|
||||||
|
<h1 class="text-6xl font-bold text-primary mb-4">{status_code}</h1>
|
||||||
|
<h2 class="text-2xl font-semibold text-white mb-4">{title}</h2>
|
||||||
|
<p class="text-secondary mb-8">{message}</p>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="inline-flex items-center justify-center px-6 py-3 rounded-lg bg-primary text-white font-medium hover:brightness-110 transition-all"
|
||||||
|
>
|
||||||
|
Go Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
80
app/javascript/pages/Extensions/Index.svelte
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<script lang="ts">
|
||||||
|
const EXTENSIONS = [
|
||||||
|
{
|
||||||
|
name: "Hackatime Desktop",
|
||||||
|
source: "https://github.com/hackclub/hackatime-desktop",
|
||||||
|
description:
|
||||||
|
"Desktop app for Hackatime. Runs on Mac, Windows, and Linux.",
|
||||||
|
install: "https://github.com/hackclub/hackatime-desktop/releases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cattatime",
|
||||||
|
source: "https://github.com/joysudo/catatime/tree/master",
|
||||||
|
description:
|
||||||
|
"A Tamagotchi system. Code, fill your cup, and get your pet rewards.",
|
||||||
|
install: "https://github.com/joysudo/catatime/releases/",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-surface-content mb-2">Extensions</h1>
|
||||||
|
<p class="text-muted">
|
||||||
|
Third-party tools created by the community.
|
||||||
|
<span class="text-primary opacity-80 italic text-sm ml-1">
|
||||||
|
* Not guaranteed to work.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{#each EXTENSIONS as extension}
|
||||||
|
<div
|
||||||
|
class="flex flex-col bg-surface border border-surface-200 rounded-lg p-5 hover:border-surface-300 transition-colors"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-start mb-1">
|
||||||
|
<h2 class="text-xl font-bold text-surface-content">
|
||||||
|
{extension.name}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted text-sm leading-relaxed mb-6 flex-1">
|
||||||
|
{extension.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-3 mt-auto">
|
||||||
|
<a
|
||||||
|
href={extension.install}
|
||||||
|
target="_blank"
|
||||||
|
class="flex items-center justify-center gap-2 px-4 py-2 rounded bg-primary text-white font-medium text-sm hover:opacity-90 transition-opacity"
|
||||||
|
>
|
||||||
|
<span>Install</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline
|
||||||
|
points="7 10 12 15 17 10"
|
||||||
|
/><line x1="12" x2="12" y1="15" y2="3" /></svg
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={extension.source}
|
||||||
|
target="_blank"
|
||||||
|
class="flex items-center justify-center gap-2 px-4 py-2 rounded bg-surface-200 text-surface-content font-medium text-sm hover:bg-surface-300 transition-colors"
|
||||||
|
>
|
||||||
|
Source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
125
app/javascript/pages/Home/SignedIn.svelte
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type {
|
||||||
|
ActivityGraphData,
|
||||||
|
} from "../../types/index";
|
||||||
|
import BanNotice from "./signedIn/BanNotice.svelte";
|
||||||
|
import GitHubLinkBanner from "./signedIn/GitHubLinkBanner.svelte";
|
||||||
|
import SetupNotice from "./signedIn/SetupNotice.svelte";
|
||||||
|
import TodaySentence from "./signedIn/TodaySentence.svelte";
|
||||||
|
import Dashboard from "./signedIn/Dashboard.svelte";
|
||||||
|
import ActivityGraph from "./signedIn/ActivityGraph.svelte";
|
||||||
|
|
||||||
|
type SocialProofUser = { display_name: string; avatar_url: string };
|
||||||
|
|
||||||
|
type FilterableDashboardData = {
|
||||||
|
total_time: number;
|
||||||
|
total_heartbeats: number;
|
||||||
|
top_project: string | null;
|
||||||
|
top_language: string | null;
|
||||||
|
top_editor: string | null;
|
||||||
|
top_operating_system: string | null;
|
||||||
|
project_durations: Record<string, number>;
|
||||||
|
language_stats: Record<string, number>;
|
||||||
|
editor_stats: Record<string, number>;
|
||||||
|
operating_system_stats: Record<string, number>;
|
||||||
|
category_stats: Record<string, number>;
|
||||||
|
weekly_project_stats: Record<string, Record<string, number>>;
|
||||||
|
project: string[];
|
||||||
|
language: string[];
|
||||||
|
editor: string[];
|
||||||
|
operating_system: string[];
|
||||||
|
category: string[];
|
||||||
|
selected_interval: string;
|
||||||
|
selected_from: string;
|
||||||
|
selected_to: string;
|
||||||
|
selected_project: string[];
|
||||||
|
selected_language: string[];
|
||||||
|
selected_editor: string[];
|
||||||
|
selected_operating_system: string[];
|
||||||
|
selected_category: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
flavor_text,
|
||||||
|
trust_level_red,
|
||||||
|
show_wakatime_setup_notice,
|
||||||
|
ssp_message,
|
||||||
|
ssp_users_recent,
|
||||||
|
ssp_users_size,
|
||||||
|
github_uid_blank,
|
||||||
|
github_auth_path,
|
||||||
|
wakatime_setup_path,
|
||||||
|
show_logged_time_sentence,
|
||||||
|
todays_duration_display,
|
||||||
|
todays_languages,
|
||||||
|
todays_editors,
|
||||||
|
filterable_dashboard_data,
|
||||||
|
activity_graph,
|
||||||
|
}: {
|
||||||
|
flavor_text: string;
|
||||||
|
trust_level_red: boolean;
|
||||||
|
show_wakatime_setup_notice: boolean;
|
||||||
|
ssp_message?: string | null;
|
||||||
|
ssp_users_recent: SocialProofUser[];
|
||||||
|
ssp_users_size: number;
|
||||||
|
github_uid_blank: boolean;
|
||||||
|
github_auth_path: string;
|
||||||
|
wakatime_setup_path: string;
|
||||||
|
show_logged_time_sentence: boolean;
|
||||||
|
todays_duration_display: string;
|
||||||
|
todays_languages: string[];
|
||||||
|
todays_editors: string[];
|
||||||
|
filterable_dashboard_data: FilterableDashboardData;
|
||||||
|
activity_graph: ActivityGraphData;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<p class="italic text-gray-400 m-0">
|
||||||
|
{@html flavor_text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="font-bold mt-2 mb-4 text-3xl md:text-4xl">
|
||||||
|
Keep Track of <span class="text-primary">Your</span> Coding Time
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if trust_level_red}
|
||||||
|
<BanNotice />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if show_wakatime_setup_notice}
|
||||||
|
<SetupNotice
|
||||||
|
{wakatime_setup_path}
|
||||||
|
{ssp_message}
|
||||||
|
{ssp_users_recent}
|
||||||
|
{ssp_users_size}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if github_uid_blank}
|
||||||
|
<GitHubLinkBanner {github_auth_path} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-8">
|
||||||
|
<!-- Today Stats & Leaderboard -->
|
||||||
|
<div>
|
||||||
|
<TodaySentence
|
||||||
|
{show_logged_time_sentence}
|
||||||
|
{todays_duration_display}
|
||||||
|
{todays_languages}
|
||||||
|
{todays_editors}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Dashboard -->
|
||||||
|
<Dashboard data={filterable_dashboard_data} />
|
||||||
|
|
||||||
|
<!-- Activity Graph -->
|
||||||
|
<ActivityGraph data={activity_graph} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
261
app/javascript/pages/Home/SignedOut.svelte
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
<script lang="ts">
|
||||||
|
type HomeStats = { seconds_tracked?: number; users_tracked?: number };
|
||||||
|
|
||||||
|
let {
|
||||||
|
hca_auth_path,
|
||||||
|
slack_auth_path,
|
||||||
|
email_auth_path,
|
||||||
|
sign_in_email,
|
||||||
|
show_dev_tool,
|
||||||
|
dev_magic_link,
|
||||||
|
csrf_token,
|
||||||
|
home_stats,
|
||||||
|
}: {
|
||||||
|
hca_auth_path: string;
|
||||||
|
slack_auth_path: string;
|
||||||
|
email_auth_path: string;
|
||||||
|
sign_in_email: boolean;
|
||||||
|
show_dev_tool: boolean;
|
||||||
|
dev_magic_link?: string | null;
|
||||||
|
csrf_token: string;
|
||||||
|
home_stats: HomeStats;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let isSigningIn = $state(false);
|
||||||
|
|
||||||
|
const editors = [
|
||||||
|
{ name: "VS Code", slug: "vs-code" },
|
||||||
|
{ name: "PyCharm", slug: "pycharm" },
|
||||||
|
{ name: "IntelliJ", slug: "intellij-idea" },
|
||||||
|
{ name: "Vim", slug: "vim" },
|
||||||
|
{ name: "Neovim", slug: "neovim" },
|
||||||
|
{ name: "Zed", slug: "zed" },
|
||||||
|
{ name: "Cursor", slug: "cursor" },
|
||||||
|
{ name: "Terminal", slug: "terminal" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const numberFormatter = new Intl.NumberFormat("en-US");
|
||||||
|
const pluralize = (count: number, singular: string, plural: string) =>
|
||||||
|
count === 1 ? singular : plural;
|
||||||
|
const formatNumber = (value: number) => numberFormatter.format(value);
|
||||||
|
|
||||||
|
const hoursTracked = $derived(
|
||||||
|
home_stats?.seconds_tracked
|
||||||
|
? Math.floor(home_stats.seconds_tracked / 3600)
|
||||||
|
: 0,
|
||||||
|
);
|
||||||
|
const usersTracked = $derived(home_stats?.users_tracked ?? 0);
|
||||||
|
|
||||||
|
// Grid background pattern
|
||||||
|
const gridPattern = `background-image: linear-gradient(to right, #4A2D3133 1px, transparent 1px), linear-gradient(to bottom, #4A2D3133 1px, transparent 1px); background-size: 6rem 6rem;`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="min-h-screen bg-darker text-white font-sans selection:bg-primary selection:text-white relative overflow-hidden flex flex-col"
|
||||||
|
>
|
||||||
|
<!-- Decorative Grid Background -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 pointer-events-none opacity-60"
|
||||||
|
style={gridPattern}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav
|
||||||
|
class="relative z-10 w-full max-w-7xl mx-auto px-6 py-6 flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 min-w-[140px]">
|
||||||
|
<img src="/images/icon-rounded.png" class="h-8 w-8" alt="Logo" />
|
||||||
|
<span class="font-bold tracking-tight text-lg">Hackatime</span>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:flex gap-8 text-sm font-medium text-text-muted">
|
||||||
|
<a href="#stats" class="hover:text-white transition-colors">Stats</a>
|
||||||
|
<a href="#editors" class="hover:text-white transition-colors">Editors</a>
|
||||||
|
<a href="/docs" class="hover:text-white transition-colors">Developers</a>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-[140px] flex justify-end">
|
||||||
|
<a
|
||||||
|
href={hca_auth_path}
|
||||||
|
class="text-sm font-bold border border-primary text-primary px-4 py-2 rounded-lg hover:bg-primary hover:text-white transition-all"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main
|
||||||
|
class="relative z-10 flex-1 flex flex-col items-center justify-center w-full max-w-4xl mx-auto px-4 pt-10 pb-20"
|
||||||
|
>
|
||||||
|
<!-- Hero Text -->
|
||||||
|
<div class="text-center mb-10 mt-4 space-y-4">
|
||||||
|
<h1 class="text-5xl font-serif tracking-tight leading-[1.1]">
|
||||||
|
The free and <br />
|
||||||
|
<span class="italic text-primary">open-source</span> coding time tracker.
|
||||||
|
</h1>
|
||||||
|
<p class="text-secondary max-w-xl mx-auto text-lg leading-relaxed">
|
||||||
|
Code stats, straight from your code editors. That's it!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth Section -->
|
||||||
|
<div class="w-full max-w-md space-y-4">
|
||||||
|
{#if sign_in_email}
|
||||||
|
<div
|
||||||
|
class="bg-dark rounded-2xl border border-darkless p-8 text-center space-y-2"
|
||||||
|
>
|
||||||
|
<p class="text-white font-medium">Check your email!</p>
|
||||||
|
<p class="text-secondary text-sm">
|
||||||
|
We sent a sign-in link to your inbox. Check your spam if you can't
|
||||||
|
see it!
|
||||||
|
</p>
|
||||||
|
{#if show_dev_tool && dev_magic_link}
|
||||||
|
<a
|
||||||
|
href={dev_magic_link}
|
||||||
|
class="text-xs text-secondary underline hover:text-white"
|
||||||
|
>Dev: Open Link</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Primary Auth Buttons -->
|
||||||
|
<a
|
||||||
|
href={hca_auth_path}
|
||||||
|
onclick={() => (isSigningIn = true)}
|
||||||
|
class="w-full flex items-center justify-center gap-3 px-6 py-3.5 rounded-xl bg-primary text-white font-medium hover:brightness-110 transition-all"
|
||||||
|
>
|
||||||
|
{#if isSigningIn}
|
||||||
|
<svg class="h-5 w-5 animate-spin" viewBox="0 0 24 24" fill="none"
|
||||||
|
><circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
></circle><path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<img
|
||||||
|
src="/images/icon-rounded.png"
|
||||||
|
class="h-5 w-5"
|
||||||
|
alt="Hack Club"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<span>Sign in with Hack Club</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={slack_auth_path}
|
||||||
|
class="w-full flex items-center justify-center gap-3 px-6 py-3.5 rounded-xl bg-dark border border-darkless text-white font-medium hover:bg-darkless transition-all"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"
|
||||||
|
><path
|
||||||
|
d="M6 15a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2h2zm1 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2a2 2 0 0 1-2-2zm2-8a2 2 0 0 1-2-2a2 2 0 0 1 2-2a2 2 0 0 1 2 2v2zm0 1a2 2 0 0 1 2 2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2a2 2 0 0 1 2-2zm8 2a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2h-2zm-1 0a2 2 0 0 1-2 2a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2a2 2 0 0 1 2 2zm-2 8a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2v-2zm0-1a2 2 0 0 1-2-2a2 2 0 0 1 2-2h5a2 2 0 0 1 2 2a2 2 0 0 1-2 2z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
<span>Sign in with Slack</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div class="flex items-center gap-4 py-1">
|
||||||
|
<div class="flex-1 h-px bg-darkless"></div>
|
||||||
|
<span class="text-xs text-secondary/60 uppercase tracking-wider"
|
||||||
|
>or</span
|
||||||
|
>
|
||||||
|
<div class="flex-1 h-px bg-darkless"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Form -->
|
||||||
|
<form method="post" action={email_auth_path} data-turbo="false">
|
||||||
|
<input type="hidden" name="authenticity_token" value={csrf_token} />
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="you@email.com"
|
||||||
|
required
|
||||||
|
class="flex-1 bg-dark text-white placeholder-secondary/40 rounded-xl py-3.5 px-4 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all border border-darkless focus:border-primary text-sm"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-5 py-3.5 bg-dark border border-primary text-white rounded-xl hover:bg-primary transition-all text-sm font-medium"
|
||||||
|
>
|
||||||
|
Send link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats / Feature Pills -->
|
||||||
|
<div class="mt-8 flex flex-wrap justify-center gap-3" id="stats">
|
||||||
|
{#if home_stats?.seconds_tracked}
|
||||||
|
<div
|
||||||
|
class="px-4 py-2 bg-dark border border-darkless rounded-lg shadow-sm"
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-white"
|
||||||
|
>{formatNumber(hoursTracked)} hours tracked</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="px-4 py-2 bg-dark border border-darkless rounded-lg shadow-sm"
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-white"
|
||||||
|
>{formatNumber(usersTracked)} hackers</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="px-4 py-2 bg-dark border border-darkless rounded-lg shadow-sm"
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-white">Works offline</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="px-4 py-2 bg-dark border border-darkless rounded-lg shadow-sm"
|
||||||
|
>
|
||||||
|
<span class="text-sm font-medium text-white">100% free</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor Logos -->
|
||||||
|
<div class="mt-20 text-center w-full" id="editors">
|
||||||
|
<h3 class="text-xl font-serif mb-8 text-secondary">
|
||||||
|
Compatible with all your favourite editors
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-4 md:grid-cols-8 gap-8 items-center justify-items-center opacity-60 hover:opacity-100 transition-all duration-500"
|
||||||
|
>
|
||||||
|
{#each editors as editor}
|
||||||
|
<a
|
||||||
|
href={`/docs/editors/${editor.slug}`}
|
||||||
|
class="group flex flex-col items-center gap-2 hover:-translate-y-1 transition-transform"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`/images/editor-icons/${editor.slug}-128.png`}
|
||||||
|
alt={editor.name}
|
||||||
|
class="w-8 h-8 object-contain"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="text-[10px] uppercase tracking-wider opacity-0 group-hover:opacity-100 transition-opacity absolute -bottom-5 text-secondary"
|
||||||
|
>{editor.name}</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 text-sm text-secondary/60">
|
||||||
|
+ 70 more supported editors
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Custom Scrollbar for cleaner look if needed */
|
||||||
|
:global(body) {
|
||||||
|
background-color: #1f1617;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
59
app/javascript/pages/Home/signedIn/ActivityGraph.svelte
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ActivityGraphData } from "../../../types/index";
|
||||||
|
|
||||||
|
let { data }: { data: ActivityGraphData } = $props();
|
||||||
|
|
||||||
|
function buildDateRange(startStr: string, endStr: string): string[] {
|
||||||
|
const dates: string[] = [];
|
||||||
|
const current = new Date(startStr + "T00:00:00");
|
||||||
|
const end = new Date(endStr + "T00:00:00");
|
||||||
|
while (current <= end) {
|
||||||
|
dates.push(current.toISOString().slice(0, 10));
|
||||||
|
current.setDate(current.getDate() + 1);
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bgColor(seconds: number, busiestDaySeconds: number): string {
|
||||||
|
if (seconds < 60) return "bg-[#151b23]";
|
||||||
|
const ratio = seconds / busiestDaySeconds;
|
||||||
|
if (ratio >= 0.8) return "bg-[#56d364]";
|
||||||
|
if (ratio >= 0.5) return "bg-[#2ea043]";
|
||||||
|
if (ratio >= 0.2) return "bg-[#196c2e]";
|
||||||
|
return "bg-[#033a16]";
|
||||||
|
}
|
||||||
|
|
||||||
|
function durationInWords(seconds: number): string {
|
||||||
|
if (seconds < 60) return "less than a minute";
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
if (hours > 0) return `about ${hours} ${hours === 1 ? "hour" : "hours"}`;
|
||||||
|
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dates = $derived(buildDateRange(data.start_date, data.end_date));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full overflow-x-auto mt-6 pb-2.5">
|
||||||
|
<div class="grid grid-rows-7 grid-flow-col gap-1 w-full lg:w-1/2">
|
||||||
|
{#each dates as date}
|
||||||
|
{@const seconds = data.duration_by_date[date] ?? 0}
|
||||||
|
<a
|
||||||
|
class="day transition-all duration-75 w-3 h-3 rounded-sm hover:scale-110 hover:z-10 hover:shadow-md {bgColor(
|
||||||
|
seconds,
|
||||||
|
data.busiest_day_seconds,
|
||||||
|
)}"
|
||||||
|
href="?date={date}"
|
||||||
|
title="you hacked for {durationInWords(seconds)} on {date}"
|
||||||
|
data-date={date}
|
||||||
|
data-duration={durationInWords(seconds)}
|
||||||
|
>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<p class="super">
|
||||||
|
Calculated in
|
||||||
|
<a href={data.timezone_settings_path}>{data.timezone_label}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script lang="ts">
|
||||||
|
const squares = Array.from({ length: 365 });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full overflow-x-auto mt-6 pb-2.5 animate-pulse">
|
||||||
|
<div class="grid grid-rows-7 grid-flow-col gap-1 w-full lg:w-1/2">
|
||||||
|
{#each squares as _}
|
||||||
|
<div class="w-3 h-3 rounded-sm bg-darkless opacity-50"></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<p class="super mt-2">
|
||||||
|
<span class="h-3 w-48 bg-darkless rounded inline-block"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
11
app/javascript/pages/Home/signedIn/BanNotice.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="text-primary bg-red-500/10 border-2 border-red-500/20 p-4 text-center rounded-lg mb-4">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M8 14.5a6.5 6.5 0 1 0 0-13a6.5 6.5 0 0 0 0 13M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m1-5a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-.25-6.25a.75.75 0 0 0-1.5 0v3.5a.75.75 0 0 0 1.5 0z" clip-rule="evenodd" /></svg>
|
||||||
|
<span class="text-3xl font-bold block ml-2">Hold up! Your account has been banned for suspicious activity.</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-primary text-left text-lg mb-2"><b>What does this mean?</b> Your account was convicted for fraud or abuse of Hackatime, such as using methods to gain an unfair advantage on the leaderboards or attempting to manipulate your coding time in any way. This restricts your access to participate in public leaderboards, but Hackatime will still track and display your time. This may also affect your ability to participate in current and future Hack Club events.</p>
|
||||||
|
<p class="text-primary text-left text-lg mb-2"><b>What can I do?</b> Account bans are non-negotiable, and will not be removed unless determined to have been issued incorrectly. In that case, it will automatically be removed. We take fraud very seriously and have a zero-tolerance policy for abuse. If you believe this was a mistake, please DM the <a href="https://hackclub.slack.com/team/U091HC53CE8" target="_blank" class="underline">Fraud Department</a> on Slack. We do not respond in any other channel, DM or thread.</p>
|
||||||
|
<p class="text-primary text-left text-lg mb-0"><b>Can I know what caused this?</b> No. We do not disclose the patterns that were detected. Releasing this information would only benefit fraudsters. The fraud team regularly investigates claims of false bans to increase the effectiveness of our detection systems to combat fraud.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
183
app/javascript/pages/Home/signedIn/Dashboard.svelte
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { router } from "@inertiajs/svelte";
|
||||||
|
import { secondsToDisplay } from "./utils";
|
||||||
|
import StatCard from "./StatCard.svelte";
|
||||||
|
import HorizontalBarList from "./HorizontalBarList.svelte";
|
||||||
|
import PieChart from "./PieChart.svelte";
|
||||||
|
import ProjectTimelineChart from "./ProjectTimelineChart.svelte";
|
||||||
|
import IntervalSelect from "./IntervalSelect.svelte";
|
||||||
|
import MultiSelect from "./MultiSelect.svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: Record<string, any>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
const langStats = $derived(
|
||||||
|
(data.language_stats || {}) as Record<string, number>,
|
||||||
|
);
|
||||||
|
const editorStats = $derived(
|
||||||
|
(data.editor_stats || {}) as Record<string, number>,
|
||||||
|
);
|
||||||
|
const osStats = $derived(
|
||||||
|
(data.operating_system_stats || {}) as Record<string, number>,
|
||||||
|
);
|
||||||
|
const projectEntries = $derived(
|
||||||
|
Object.entries(data.project_durations || {}) as [string, number][],
|
||||||
|
);
|
||||||
|
const weeklyStats = $derived(
|
||||||
|
(data.weekly_project_stats || {}) as Record<string, Record<string, number>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const capitalize = (s: string) =>
|
||||||
|
s ? s.charAt(0).toUpperCase() + s.slice(1) : "";
|
||||||
|
|
||||||
|
function applyFilters(overrides: Record<string, string>) {
|
||||||
|
const current = new URL(window.location.href);
|
||||||
|
for (const [k, v] of Object.entries(overrides)) {
|
||||||
|
if (v) {
|
||||||
|
current.searchParams.set(k, v);
|
||||||
|
} else {
|
||||||
|
current.searchParams.delete(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
router.get(
|
||||||
|
current.pathname + current.search,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
preserveState: true,
|
||||||
|
preserveScroll: true,
|
||||||
|
only: ["filterable_dashboard_data"],
|
||||||
|
onFinish: () => (loading = false),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIntervalChange(interval: string, from: string, to: string) {
|
||||||
|
if (from || to) {
|
||||||
|
applyFilters({ interval: "custom", from, to });
|
||||||
|
} else {
|
||||||
|
applyFilters({ interval, from: "", to: "" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFilterChange(param: string, selected: string[]) {
|
||||||
|
applyFilters({ [param]: selected.join(",") });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-6 w-full" class:opacity-60={loading}>
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3 mb-2">
|
||||||
|
<IntervalSelect
|
||||||
|
selected={data.selected_interval || ""}
|
||||||
|
from={data.selected_from || ""}
|
||||||
|
to={data.selected_to || ""}
|
||||||
|
onchange={onIntervalChange}
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label="Project"
|
||||||
|
param="project"
|
||||||
|
values={data.project || []}
|
||||||
|
selected={data.selected_project || []}
|
||||||
|
onchange={(s) => onFilterChange("project", s)}
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label="Language"
|
||||||
|
param="language"
|
||||||
|
values={data.language || []}
|
||||||
|
selected={data.selected_language || []}
|
||||||
|
onchange={(s) => onFilterChange("language", s)}
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label="OS"
|
||||||
|
param="operating_system"
|
||||||
|
values={data.operating_system || []}
|
||||||
|
selected={data.selected_operating_system || []}
|
||||||
|
onchange={(s) => onFilterChange("operating_system", s)}
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label="Editor"
|
||||||
|
param="editor"
|
||||||
|
values={data.editor || []}
|
||||||
|
selected={data.selected_editor || []}
|
||||||
|
onchange={(s) => onFilterChange("editor", s)}
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label="Category"
|
||||||
|
param="category"
|
||||||
|
values={data.category || []}
|
||||||
|
selected={data.selected_category || []}
|
||||||
|
onchange={(s) => onFilterChange("category", s)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Grid -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
|
<StatCard
|
||||||
|
label="Total Time"
|
||||||
|
value={secondsToDisplay(data.total_time)}
|
||||||
|
highlight
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Top Project"
|
||||||
|
value={data.top_project || "None"}
|
||||||
|
subtitle={data.singular_project ? "obviously" : ""}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Top Language"
|
||||||
|
value={data.top_language || "Unknown"}
|
||||||
|
subtitle={data.singular_language ? "obviously" : ""}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Top OS"
|
||||||
|
value={data.top_operating_system || "Unknown"}
|
||||||
|
subtitle={data.singular_operating_system ? "obviously" : ""}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Top Editor"
|
||||||
|
value={data.top_editor || "Unknown"}
|
||||||
|
subtitle={data.singular_editor ? "obviously" : ""}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Top Category"
|
||||||
|
value={capitalize(data.top_category) || "Unknown"}
|
||||||
|
subtitle={data.singular_category ? "obviously" : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts Layout -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 w-full">
|
||||||
|
{#if projectEntries.length > 1}
|
||||||
|
<div class="lg:col-span-1">
|
||||||
|
<HorizontalBarList
|
||||||
|
title="Project Durations"
|
||||||
|
entries={projectEntries}
|
||||||
|
empty_message="No data yet."
|
||||||
|
useLogScale
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if Object.keys(langStats).length > 0}
|
||||||
|
<PieChart title="Languages" stats={langStats} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if Object.keys(editorStats).length > 0}
|
||||||
|
<PieChart title="Editors" stats={editorStats} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if Object.keys(osStats).length > 0}
|
||||||
|
<PieChart title="Operating Systems" stats={osStats} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<ProjectTimelineChart {weeklyStats} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
14
app/javascript/pages/Home/signedIn/DashboardSkeleton.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script lang="ts">
|
||||||
|
const filterRows = Array.from({ length: 6 });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="max-w-6xl mx-auto my-0 animate-pulse">
|
||||||
|
<div class="flex gap-4 mt-2 mb-6 flex-wrap">
|
||||||
|
{#each filterRows as _}
|
||||||
|
<div class="filter flex-1 min-w-37.5 relative">
|
||||||
|
<div class="h-3 w-16 bg-darkless rounded mb-1.5"></div>
|
||||||
|
<div class="h-10 w-full bg-darkless rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
30
app/javascript/pages/Home/signedIn/GitHubLinkBanner.svelte
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let { github_auth_path }: { github_auth_path: string } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-dark border border-primary rounded-lg p-4 mb-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="text-white shrink-0"
|
||||||
|
><path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<span class="text-white"
|
||||||
|
>Link your GitHub account to unlock project linking, show what
|
||||||
|
you're working on, and qualify for leaderboards!</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={github_auth_path}
|
||||||
|
class="bg-primary hover:bg-primary text-white font-medium px-4 py-2 rounded-lg transition-colors duration-200 shrink-0"
|
||||||
|
data-turbo="false">Connect GitHub</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
51
app/javascript/pages/Home/signedIn/HorizontalBarList.svelte
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { secondsToDisplay, percentOf, logScale } from "./utils";
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
entries,
|
||||||
|
empty_message = "No data yet.",
|
||||||
|
useLogScale = false,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
entries: [string, number][];
|
||||||
|
empty_message?: string;
|
||||||
|
useLogScale?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const maxVal = $derived(Math.max(...entries.map(([_, v]) => v || 0), 1));
|
||||||
|
const barWidth = (seconds: number) =>
|
||||||
|
useLogScale ? logScale(seconds, maxVal) : percentOf(seconds, maxVal);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-dark/50 border border-white/10 rounded-xl p-6 flex flex-col h-full"
|
||||||
|
>
|
||||||
|
<h3 class="text-lg font-semibold mb-4 text-white/90">{title}</h3>
|
||||||
|
{#if entries.length > 0}
|
||||||
|
<div class="space-y-2.5 overflow-y-auto flex-1 pr-2 custom-scrollbar">
|
||||||
|
{#each entries as [label, seconds]}
|
||||||
|
<div class="flex items-center gap-4 group">
|
||||||
|
<div
|
||||||
|
class="w-1/3 truncate font-medium text-sm text-gray-300 text-right group-hover:text-white transition-colors"
|
||||||
|
title={label}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 relative">
|
||||||
|
<div
|
||||||
|
class="bg-primary rounded-md h-6 flex items-center justify-end px-3 transition-all duration-500 ease-out"
|
||||||
|
style={`width:${Math.max(barWidth(seconds), 15)}%`}
|
||||||
|
>
|
||||||
|
<span class="text-xs font-mono text-white whitespace-nowrap">
|
||||||
|
{secondsToDisplay(seconds)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-muted text-sm italic">{empty_message}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
156
app/javascript/pages/Home/signedIn/IntervalSelect.svelte
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
<script lang="ts">
|
||||||
|
const INTERVALS = [
|
||||||
|
{ key: "today", label: "Today" },
|
||||||
|
{ key: "yesterday", label: "Yesterday" },
|
||||||
|
{ key: "this_week", label: "This Week" },
|
||||||
|
{ key: "last_7_days", label: "Last 7 Days" },
|
||||||
|
{ key: "this_month", label: "This Month" },
|
||||||
|
{ key: "last_30_days", label: "Last 30 Days" },
|
||||||
|
{ key: "this_year", label: "This Year" },
|
||||||
|
{ key: "last_12_months", label: "Last 12 Months" },
|
||||||
|
{ key: "flavortown", label: "Flavortown" },
|
||||||
|
{ key: "summer_of_making", label: "Summer of Making" },
|
||||||
|
{ key: "high_seas", label: "High Seas" },
|
||||||
|
{ key: "low_skies", label: "Low Skies" },
|
||||||
|
{ key: "scrapyard", label: "Scrapyard Global" },
|
||||||
|
{ key: "", label: "All Time" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
let {
|
||||||
|
selected,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
onchange,
|
||||||
|
}: {
|
||||||
|
selected: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
onchange: (interval: string, from: string, to: string) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let customFrom = $state("");
|
||||||
|
let customTo = $state("");
|
||||||
|
let container: HTMLDivElement | undefined = $state();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
customFrom = from;
|
||||||
|
customTo = to;
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayLabel = $derived.by(() => {
|
||||||
|
if (selected && selected !== "custom") {
|
||||||
|
return INTERVALS.find((i) => i.key === selected)?.label ?? selected;
|
||||||
|
}
|
||||||
|
if (from && to) return `${from} to ${to}`;
|
||||||
|
if (from) return `From ${from}`;
|
||||||
|
if (to) return `Until ${to}`;
|
||||||
|
return "All Time";
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDefault = $derived(!selected && !from && !to);
|
||||||
|
|
||||||
|
function handleClickOutside(e: MouseEvent) {
|
||||||
|
if (container && !container.contains(e.target as Node)) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (open) {
|
||||||
|
document.addEventListener("click", handleClickOutside, true);
|
||||||
|
return () => document.removeEventListener("click", handleClickOutside, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectInterval(key: string) {
|
||||||
|
onchange(key, "", "");
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCustomRange() {
|
||||||
|
onchange("", customFrom, customTo);
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
onchange("", "", "");
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="filter relative" bind:this={container}>
|
||||||
|
<span class="block text-xs font-medium mb-1.5 text-secondary/80 uppercase tracking-wider">
|
||||||
|
Date Range
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="group flex items-center border border-white/10 rounded-lg bg-darkless m-0 p-0 transition-all duration-200 hover:border-white/20">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-1 px-3 py-2.5 text-sm cursor-pointer select-none text-gray-300 m-0 bg-transparent flex items-center justify-between border-0"
|
||||||
|
onclick={() => (open = !open)}
|
||||||
|
>
|
||||||
|
<span>{displayLabel}</span>
|
||||||
|
<svg class="w-4 h-4 text-secondary/60 transition-transform duration-200 group-hover:text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if !isDefault}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-2.5 py-2 text-sm leading-none text-secondary/60 bg-transparent border-0 border-l border-white/10 cursor-pointer m-0 hover:text-red hover:bg-red/10 transition-colors duration-150"
|
||||||
|
onclick={clear}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div class="absolute top-full left-0 right-0 min-w-64 bg-darkless border border-white/10 rounded-lg mt-2 shadow-xl shadow-black/50 z-1000 p-2">
|
||||||
|
<div class="overflow-y-auto m-0 max-h-56">
|
||||||
|
{#each INTERVALS as interval}
|
||||||
|
<label class="flex items-center px-3 py-2.5 cursor-pointer text-sm text-gray-300 m-0 bg-transparent rounded-md hover:bg-dark transition-colors duration-150">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="interval"
|
||||||
|
class="mr-3 mb-0 h-4 w-4 min-w-4 appearance-none border border-white/20 rounded-full bg-dark relative cursor-pointer p-0 checked:bg-primary checked:border-primary hover:border-white/40 transition-colors duration-150"
|
||||||
|
checked={selected === interval.key && !from && !to}
|
||||||
|
onchange={() => selectInterval(interval.key)}
|
||||||
|
/>
|
||||||
|
{interval.label}
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-2 border-white/10" />
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2.5 pt-1">
|
||||||
|
<label class="flex items-center justify-between text-sm text-gray-300">
|
||||||
|
<span class="text-secondary/80">Start</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
class="ml-2 py-2 px-3 bg-dark border border-white/10 rounded-md text-sm text-gray-200 focus:outline-none focus:border-white/20 transition-colors duration-150"
|
||||||
|
bind:value={customFrom}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center justify-between text-sm text-gray-300">
|
||||||
|
<span class="text-secondary/80">End</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
class="ml-2 py-2 px-3 bg-dark border border-white/10 rounded-md text-sm text-gray-200 focus:outline-none focus:border-white/20 transition-colors duration-150"
|
||||||
|
bind:value={customTo}
|
||||||
|
/>
|
||||||
|
</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-white hover:bg-primary/90 border-0"
|
||||||
|
onclick={applyCustomRange}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
131
app/javascript/pages/Home/signedIn/MultiSelect.svelte
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
label,
|
||||||
|
param,
|
||||||
|
values,
|
||||||
|
selected,
|
||||||
|
onchange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
param: string;
|
||||||
|
values: string[];
|
||||||
|
selected: string[];
|
||||||
|
onchange: (selected: string[]) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let search = $state("");
|
||||||
|
let container: HTMLDivElement | undefined = $state();
|
||||||
|
|
||||||
|
let filtered = $derived(
|
||||||
|
search
|
||||||
|
? values.filter((v) => v.toLowerCase().includes(search.toLowerCase()))
|
||||||
|
: values,
|
||||||
|
);
|
||||||
|
|
||||||
|
let displayText = $derived(
|
||||||
|
selected.length === 0
|
||||||
|
? `Filter by ${label}...`
|
||||||
|
: selected.length === 1
|
||||||
|
? selected[0]
|
||||||
|
: `${selected.length} selected`,
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggle(value: string) {
|
||||||
|
if (selected.includes(value)) {
|
||||||
|
onchange(selected.filter((s) => s !== value));
|
||||||
|
} else {
|
||||||
|
onchange([...selected, value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear(e: MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onchange([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(e: MouseEvent) {
|
||||||
|
if (container && !container.contains(e.target as Node)) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (open) {
|
||||||
|
document.addEventListener("click", handleClickOutside, true);
|
||||||
|
return () =>
|
||||||
|
document.removeEventListener("click", handleClickOutside, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!open) {
|
||||||
|
search = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="filter relative" bind:this={container}>
|
||||||
|
<span class="block text-xs font-medium mb-1.5 text-secondary/80 uppercase tracking-wider">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="group flex items-center border border-white/10 rounded-lg bg-darkless m-0 p-0 transition-all duration-200 hover:border-white/20">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-1 px-3 py-2.5 text-sm cursor-pointer select-none text-gray-300 m-0 bg-transparent flex items-center justify-between border-0 min-w-0"
|
||||||
|
onclick={() => (open = !open)}
|
||||||
|
>
|
||||||
|
<span class="truncate {selected.length === 0 ? 'text-secondary/60' : ''}">
|
||||||
|
{displayText}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class={`w-4 h-4 text-secondary/60 transition-transform duration-200 group-hover:text-secondary ${open ? "rotate-180" : ""}`}
|
||||||
|
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" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if selected.length > 0}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-2.5 py-2 text-sm leading-none text-secondary/60 bg-transparent border-0 border-l border-white/10 cursor-pointer m-0 hover:text-red hover:bg-red/10 transition-colors duration-150"
|
||||||
|
onclick={clear}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div class="absolute top-full left-0 right-0 min-w-64 bg-darkless border border-white/10 rounded-lg mt-2 shadow-xl shadow-black/50 z-1000 p-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
class="w-full border border-white/10 px-3 py-2.5 mb-2 bg-dark text-white text-sm rounded-md h-auto placeholder:text-secondary/60 focus:outline-none focus:border-white/20"
|
||||||
|
bind:value={search}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="overflow-y-auto m-0 max-h-64">
|
||||||
|
{#each filtered as value}
|
||||||
|
<label class="flex items-center px-3 py-2.5 cursor-pointer text-sm text-gray-300 m-0 bg-transparent rounded-md hover:bg-dark transition-colors duration-150">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected.includes(value)}
|
||||||
|
onchange={() => toggle(value)}
|
||||||
|
class="mr-3 mb-0 h-4 w-4 min-w-4 appearance-none border border-white/20 rounded bg-dark relative cursor-pointer p-0 checked:bg-primary checked:border-primary hover:border-white/40 transition-colors duration-150"
|
||||||
|
/>
|
||||||
|
{value}
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if filtered.length === 0}
|
||||||
|
<div class="px-3 py-2.5 text-sm text-secondary/60">No results</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
75
app/javascript/pages/Home/signedIn/PieChart.svelte
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { PieChart } from "layerchart";
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
stats,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
stats: Record<string, number>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const PIE_COLORS = [
|
||||||
|
"#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 data = $derived(
|
||||||
|
Object.entries(stats).map(([name, value]) => ({ name, value })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const legendClasses = {
|
||||||
|
root: "w-full px-2",
|
||||||
|
swatches: "flex-wrap justify-center",
|
||||||
|
label: "text-xs text-white/70",
|
||||||
|
};
|
||||||
|
|
||||||
|
const legendPadding = $derived.by(() => {
|
||||||
|
const rows = Math.max(1, Math.ceil(data.length / 4));
|
||||||
|
return Math.min(96, 24 + rows * 18);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-dark/50 border border-white/10 rounded-xl p-6 flex flex-col h-full"
|
||||||
|
>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-white/90">{title}</h2>
|
||||||
|
<div class="h-[260px] sm:h-[280px] lg:h-[300px]">
|
||||||
|
{#if data.length > 0}
|
||||||
|
<PieChart
|
||||||
|
{data}
|
||||||
|
key="name"
|
||||||
|
value="value"
|
||||||
|
cRange={PIE_COLORS}
|
||||||
|
legend={true}
|
||||||
|
padding={{ bottom: legendPadding }}
|
||||||
|
props={{
|
||||||
|
legend: { classes: legendClasses },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
44
app/javascript/pages/Home/signedIn/ProjectTimeline.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { secondsToDisplay, percentOf } from "./utils";
|
||||||
|
|
||||||
|
let {
|
||||||
|
entries,
|
||||||
|
}: {
|
||||||
|
entries: [string, Record<string, number>][];
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-dark border border-primary rounded-xl p-6 flex flex-col md:col-span-2"
|
||||||
|
>
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Project Timeline</h3>
|
||||||
|
{#if entries.length > 0}
|
||||||
|
<div class="flex flex-col gap-2 max-h-96 overflow-y-auto">
|
||||||
|
{#each entries as [week, stats]}
|
||||||
|
{@const total = Object.values(stats).reduce(
|
||||||
|
(a, v) => a + (v || 0),
|
||||||
|
0,
|
||||||
|
)}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-28 text-sm text-muted">{week}</div>
|
||||||
|
<div class="flex-1 bg-darkless rounded h-3 overflow-hidden">
|
||||||
|
<div class="flex h-3 w-full">
|
||||||
|
{#each Object.entries(stats) as [project, seconds]}
|
||||||
|
<div
|
||||||
|
class="h-3 bg-primary/80"
|
||||||
|
style={`width:${percentOf(seconds, total)}%`}
|
||||||
|
title={`${project}: ${secondsToDisplay(seconds)}`}
|
||||||
|
></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-16 text-sm text-muted text-right">
|
||||||
|
{secondsToDisplay(total)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-muted">No timeline data.</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
108
app/javascript/pages/Home/signedIn/ProjectTimelineChart.svelte
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { BarChart } from "layerchart";
|
||||||
|
|
||||||
|
let {
|
||||||
|
weeklyStats,
|
||||||
|
}: {
|
||||||
|
weeklyStats: Record<string, Record<string, number>>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const PIE_COLORS = [
|
||||||
|
"#60a5fa", "#f472b6", "#fb923c", "#facc15", "#4ade80",
|
||||||
|
"#2dd4bf", "#a78bfa", "#f87171", "#38bdf8", "#e879f9",
|
||||||
|
"#34d399", "#fbbf24", "#818cf8", "#fb7185", "#22d3ee",
|
||||||
|
"#a3e635", "#c084fc", "#f97316", "#14b8a6", "#8b5cf6",
|
||||||
|
];
|
||||||
|
|
||||||
|
const sortedWeeks = $derived(Object.keys(weeklyStats).sort());
|
||||||
|
|
||||||
|
const allProjects = $derived.by(() => {
|
||||||
|
const projectTotals = new Map<string, number>();
|
||||||
|
for (const weekData of Object.values(weeklyStats)) {
|
||||||
|
for (const [project, seconds] of Object.entries(weekData)) {
|
||||||
|
projectTotals.set(project, (projectTotals.get(project) || 0) + seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(projectTotals.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([name]) => name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = $derived(
|
||||||
|
sortedWeeks.map((week) => {
|
||||||
|
const row: Record<string, string | number> = {
|
||||||
|
week: new Date(week).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
for (const project of allProjects) {
|
||||||
|
row[project] = weeklyStats[week][project] || 0;
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const series = $derived(
|
||||||
|
allProjects.map((project, i) => ({
|
||||||
|
key: project,
|
||||||
|
label: project,
|
||||||
|
color: PIE_COLORS[i % PIE_COLORS.length],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const legendClasses = {
|
||||||
|
root: "w-full px-2",
|
||||||
|
swatches: "flex-wrap justify-center",
|
||||||
|
label: "text-xs text-white/70",
|
||||||
|
};
|
||||||
|
|
||||||
|
const legendPadding = $derived.by(() => {
|
||||||
|
const rows = Math.max(1, Math.ceil(series.length / 4));
|
||||||
|
return Math.min(120, 24 + rows * 18);
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartPadding = $derived.by(() => ({
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
left: 20,
|
||||||
|
bottom: 20 + legendPadding,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function formatDuration(value: number): string {
|
||||||
|
if (value === 0) return "0s";
|
||||||
|
const hours = Math.floor(value / 3600);
|
||||||
|
const minutes = Math.floor((value % 3600) / 60);
|
||||||
|
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatYAxis(value: number): string {
|
||||||
|
if (value === 0) return "0s";
|
||||||
|
const hours = Math.floor(value / 3600);
|
||||||
|
const minutes = Math.floor((value % 3600) / 60);
|
||||||
|
return hours > 0 ? `${hours}h` : `${minutes}m`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-dark/50 border border-white/10 rounded-xl p-6 flex flex-col min-h-[400px]"
|
||||||
|
>
|
||||||
|
<h2 class="mb-4 text-lg font-semibold text-white/90">Project Timeline</h2>
|
||||||
|
{#if data.length > 0}
|
||||||
|
<div class="h-[350px]">
|
||||||
|
<BarChart
|
||||||
|
{data}
|
||||||
|
x="week"
|
||||||
|
{series}
|
||||||
|
seriesLayout="stack"
|
||||||
|
legend
|
||||||
|
padding={chartPadding}
|
||||||
|
props={{
|
||||||
|
yAxis: { format: formatYAxis },
|
||||||
|
tooltip: { item: { format: formatDuration } },
|
||||||
|
legend: { classes: legendClasses },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
34
app/javascript/pages/Home/signedIn/SetupNotice.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import SocialProofUsers from "./SocialProofUsers.svelte";
|
||||||
|
|
||||||
|
type SocialProofUser = { display_name: string; avatar_url: string };
|
||||||
|
|
||||||
|
let {
|
||||||
|
wakatime_setup_path,
|
||||||
|
ssp_message,
|
||||||
|
ssp_users_recent,
|
||||||
|
ssp_users_size,
|
||||||
|
}: {
|
||||||
|
wakatime_setup_path: string;
|
||||||
|
ssp_message?: string | null;
|
||||||
|
ssp_users_recent: SocialProofUser[];
|
||||||
|
ssp_users_size: number;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="text-left my-8 flex flex-col">
|
||||||
|
<p class="mb-4 text-xl text-primary">
|
||||||
|
Hello friend! Looks like you are new around here, let's get you set up
|
||||||
|
so you can start tracking your coding time.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={wakatime_setup_path}
|
||||||
|
class="inline-block w-auto text-3xl font-bold px-8 py-4 bg-primary text-white rounded shadow-md hover:shadow-lg hover:-translate-y-1 transition-all duration-300 animate-pulse"
|
||||||
|
>Let's setup Hackatime! Click me :D</a
|
||||||
|
>
|
||||||
|
<SocialProofUsers
|
||||||
|
users={ssp_users_recent}
|
||||||
|
total_size={ssp_users_size}
|
||||||
|
message={ssp_message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
82
app/javascript/pages/Home/signedIn/SocialProofUsers.svelte
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script lang="ts">
|
||||||
|
type SocialProofUser = { display_name: string; avatar_url: string };
|
||||||
|
|
||||||
|
let {
|
||||||
|
users,
|
||||||
|
total_size,
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
users: SocialProofUser[];
|
||||||
|
total_size: number;
|
||||||
|
message?: string | null;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center mt-4 flex-nowrap">
|
||||||
|
{#if users.length > 0}
|
||||||
|
<div class="flex m-0 ml-0 shrink-0">
|
||||||
|
{#each users as user, index}
|
||||||
|
<div
|
||||||
|
class={`relative cursor-pointer transition-transform duration-200 hover:-translate-y-1 hover:z-10 group ${index > 0 ? "-ml-4" : ""}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute -top-9 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-2 py-1 rounded text-xs whitespace-nowrap opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-20"
|
||||||
|
>
|
||||||
|
{user.display_name}
|
||||||
|
<div
|
||||||
|
class="absolute top-full left-1/2 -ml-1 border-l-2 border-r-2 border-t-2 border-transparent border-t-gray-800"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={user.avatar_url}
|
||||||
|
alt={user.display_name}
|
||||||
|
class="w-10 h-10 rounded-full border-2 border-primary object-cover shadow-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if total_size > 5}
|
||||||
|
<div
|
||||||
|
class="relative cursor-pointer transition-transform duration-200 hover:-translate-y-1 hover:z-10 group -ml-4"
|
||||||
|
title={`See all ${total_size} users`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-full border-2 border-primary bg-primary text-white font-bold text-sm flex items-center justify-center shadow-sm"
|
||||||
|
>
|
||||||
|
+{total_size - 5}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute -left-5 top-11 bg-gray-800 rounded-lg shadow-xl p-4 w-80 z-50 max-h-96 overflow-y-auto opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
class="mt-0 mb-2 text-base text-gray-200 border-b border-gray-600 pb-2"
|
||||||
|
>
|
||||||
|
All users who set up Hackatime
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each users as user}
|
||||||
|
<div
|
||||||
|
class="flex items-center p-1 rounded hover:bg-gray-700 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={user.avatar_url}
|
||||||
|
alt={user.display_name}
|
||||||
|
class="w-8 h-8 rounded-full mr-2 border border-primary"
|
||||||
|
/>
|
||||||
|
<span class="font-medium text-sm">{user.display_name}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute -top-2 left-8 w-0 h-0 border-l-2 border-r-2 border-b-2 border-transparent border-b-gray-800"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if message}
|
||||||
|
<p class="m-0 ml-2 italic text-gray-400">
|
||||||
|
{message} (this is real data)
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
40
app/javascript/pages/Home/signedIn/StatCard.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
highlight = false,
|
||||||
|
subtitle = "",
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
highlight?: boolean;
|
||||||
|
subtitle?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={`
|
||||||
|
relative flex flex-col justify-between p-4 pb-6 rounded-xl border transition-all duration-200 h-full
|
||||||
|
${
|
||||||
|
highlight
|
||||||
|
? "bg-primary/10 border-primary/30"
|
||||||
|
: "bg-white/5 border-white/10 hover:border-white/20"
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="text-secondary/80 text-xs font-medium uppercase tracking-wider mb-2"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`font-bold text-white wrap-break-word ${String(value).length > 15 ? "text-lg" : "text-xl"}`}
|
||||||
|
>
|
||||||
|
{value || "—"}
|
||||||
|
</div>
|
||||||
|
{#if subtitle}
|
||||||
|
<span class="absolute bottom-2 left-4 text-xs text-secondary font-normal opacity-70"
|
||||||
|
>{subtitle}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
48
app/javascript/pages/Home/signedIn/TodaySentence.svelte
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { pluralize, toSentence } from "./utils";
|
||||||
|
|
||||||
|
let {
|
||||||
|
show_logged_time_sentence,
|
||||||
|
todays_duration_display,
|
||||||
|
todays_languages,
|
||||||
|
todays_editors,
|
||||||
|
}: {
|
||||||
|
show_logged_time_sentence: boolean;
|
||||||
|
todays_duration_display: string;
|
||||||
|
todays_languages: string[];
|
||||||
|
todays_editors: string[];
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{#if show_logged_time_sentence}
|
||||||
|
You've logged
|
||||||
|
{todays_duration_display}
|
||||||
|
{#if todays_languages.length > 0 || todays_editors.length > 0}
|
||||||
|
across
|
||||||
|
{#if todays_languages.length > 0}
|
||||||
|
{#if todays_languages.length >= 4}
|
||||||
|
{todays_languages.slice(0, 2).join(", ")}
|
||||||
|
<span title={todays_languages.slice(2).join(", ")}>
|
||||||
|
(& {todays_languages.length - 2}
|
||||||
|
{pluralize(
|
||||||
|
todays_languages.length - 2,
|
||||||
|
"other language",
|
||||||
|
"other languages",
|
||||||
|
)})
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
{toSentence(todays_languages)}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if todays_languages.length > 0 && todays_editors.length > 0}
|
||||||
|
using
|
||||||
|
{/if}
|
||||||
|
{#if todays_editors.length > 0}
|
||||||
|
{toSentence(todays_editors)}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
No time logged today... but you can change that!
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
36
app/javascript/pages/Home/signedIn/utils.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
export const pluralize = (count: number, singular: string, plural: string) =>
|
||||||
|
count === 1 ? singular : plural;
|
||||||
|
|
||||||
|
export const toSentence = (items: string[]) => {
|
||||||
|
if (items.length === 0) return "";
|
||||||
|
if (items.length === 1) return items[0];
|
||||||
|
if (items.length === 2) return `${items[0]} and ${items[1]}`;
|
||||||
|
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const secondsToDisplay = (seconds?: number) => {
|
||||||
|
if (!seconds) return "0m";
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const percentOf = (value: number, max: number) => {
|
||||||
|
if (!max || max === 0) return 0;
|
||||||
|
return Math.max(2, Math.round((value / max) * 100));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logScale = (value: number, maxVal: number): number => {
|
||||||
|
if (value === 0) return 0;
|
||||||
|
const minPercent = 5;
|
||||||
|
const maxPercent = 100;
|
||||||
|
const linearRatio = value / maxVal;
|
||||||
|
const logRatio = Math.log(value + 1) / Math.log(maxVal + 1);
|
||||||
|
const linearWeight = 0.8;
|
||||||
|
const logWeight = 0.2;
|
||||||
|
const scaled =
|
||||||
|
minPercent +
|
||||||
|
(linearWeight * linearRatio + logWeight * logRatio) *
|
||||||
|
(maxPercent - minPercent);
|
||||||
|
return Math.min(Math.round(scaled), maxPercent);
|
||||||
|
};
|
||||||
368
app/javascript/pages/WakatimeSetup/Index.svelte
Normal file
|
|
@ -0,0 +1,368 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Link } from "@inertiajs/svelte";
|
||||||
|
import Stepper from "./Stepper.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
current_user_api_key: string;
|
||||||
|
setup_os: string;
|
||||||
|
api_url: string;
|
||||||
|
heartbeat_check_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { current_user_api_key, setup_os, api_url, heartbeat_check_url }: Props =
|
||||||
|
$props();
|
||||||
|
|
||||||
|
let activeSection = $derived(
|
||||||
|
setup_os === "windows" ? "windows" : "mac-linux",
|
||||||
|
);
|
||||||
|
let hasHeartbeat = $state(false);
|
||||||
|
let heartbeatTimeAgo = $state("");
|
||||||
|
let checkCount = $state(0);
|
||||||
|
let statusMessage = $state(
|
||||||
|
"Run the command above, then we'll automatically detect when you're ready.",
|
||||||
|
);
|
||||||
|
let statusPanelClass = $state("border-darkless");
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
"Copy the command above and run it in your terminal!",
|
||||||
|
"Paste the command and press Enter...",
|
||||||
|
"The script will configure everything automatically!",
|
||||||
|
"Almost there - just run the command!",
|
||||||
|
"We'll detect it as soon as the script runs!",
|
||||||
|
];
|
||||||
|
|
||||||
|
const sharedTitle = "Configure Hackatime";
|
||||||
|
const sharedSubtitle =
|
||||||
|
"This creates your config file and validates your API key. And if you're using VS Code, a JetBrains IDE, Zed, or Xcode, we'll even set up the plugins for you!";
|
||||||
|
|
||||||
|
function toggleSection(section: string) {
|
||||||
|
activeSection = section;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccess(timeAgo: string) {
|
||||||
|
hasHeartbeat = true;
|
||||||
|
heartbeatTimeAgo = timeAgo;
|
||||||
|
statusPanelClass = "border-green bg-green/5";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkHeartbeat() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(heartbeat_check_url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${current_user_api_key}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.has_heartbeat) {
|
||||||
|
const heartbeatTime = new Date(data.heartbeat.created_at);
|
||||||
|
const now = new Date();
|
||||||
|
const secondsAgo = (now.getTime() - heartbeatTime.getTime()) / 1000;
|
||||||
|
const recentThreshold = 300;
|
||||||
|
|
||||||
|
if (secondsAgo <= recentThreshold) {
|
||||||
|
showSuccess(data.time_ago);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("No heartbeats yet");
|
||||||
|
} catch (error) {
|
||||||
|
checkCount++;
|
||||||
|
|
||||||
|
if (checkCount % 3 === 0) {
|
||||||
|
const msgIndex = Math.floor(checkCount / 3) % messages.length;
|
||||||
|
statusMessage = messages[msgIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
checkHeartbeat();
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (!hasHeartbeat) {
|
||||||
|
checkHeartbeat();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Configure Hackatime - Step 1</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="min-h-screen text-white pt-8 pb-16">
|
||||||
|
<div class="max-w-2xl mx-auto px-4">
|
||||||
|
<Stepper currentStep={1} />
|
||||||
|
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div
|
||||||
|
class="border border-darkless rounded-xl p-6 bg-dark transition-all duration-300 {statusPanelClass}"
|
||||||
|
>
|
||||||
|
{#if !hasHeartbeat}
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center text-center py-2"
|
||||||
|
>
|
||||||
|
<h4 class="text-lg font-semibold text-white mb-1">
|
||||||
|
Waiting for setup...
|
||||||
|
</h4>
|
||||||
|
<p class="text-sm text-secondary mb-4 max-w-sm">{statusMessage}</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-center py-2">
|
||||||
|
<h4 class="text-xl font-bold text-white">Setup complete!</h4>
|
||||||
|
<p class="text-secondary text-sm mb-6">
|
||||||
|
Heartbeat detected {heartbeatTimeAgo}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/my/wakatime_setup/step-2"
|
||||||
|
class="inline-flex items-center justify-center bg-primary text-white px-6 py-2 rounded-lg font-semibold transition-all"
|
||||||
|
>
|
||||||
|
Continue to Step 2 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if activeSection === "mac-linux"}
|
||||||
|
<div class="bg-dark border border-darkless rounded-xl p-8 shadow-sm">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-2">{sharedTitle}</h3>
|
||||||
|
<p class="text-secondary text-sm">{sharedSubtitle}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-blue/5 border border-blue/20 rounded-lg p-4 mb-6 flex gap-3"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-blue shrink-0 mt-0.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class="text-sm">
|
||||||
|
<p class="font-medium text-blue mb-1">Using GitHub Codespaces?</p>
|
||||||
|
<p class="text-secondary">
|
||||||
|
Look for the <strong>Terminal</strong> tab at the bottom of your
|
||||||
|
window. If you don't see it, press
|
||||||
|
<kbd
|
||||||
|
class="bg-darkless text-white px-1.5 py-0.5 rounded text-xs font-mono"
|
||||||
|
>Ctrl+`</kbd
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-6 h-6 rounded-full bg-darkless text-white flex items-center justify-center text-xs font-bold mt-0.5"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium mb-1">Open your terminal</p>
|
||||||
|
<p class="text-sm text-secondary">
|
||||||
|
Search for "Terminal" in Spotlight (Mac) or your applications
|
||||||
|
menu.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-6 h-6 rounded-full bg-darkless text-white flex items-center justify-center text-xs font-bold mt-0.5"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<p class="font-medium mb-1">Run the install command</p>
|
||||||
|
<div class="relative group mt-2">
|
||||||
|
<div
|
||||||
|
class="bg-darker border border-darkless rounded-lg overflow-x-auto"
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
class="p-4 pr-20 text-sm font-mono text-cyan whitespace-pre-wrap break-all"><code
|
||||||
|
>curl -fsSL https://hack.club/setup/install.sh | bash -s -- {current_user_api_key}</code
|
||||||
|
></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 pt-6 border-t border-darkless">
|
||||||
|
<details class="group">
|
||||||
|
<summary
|
||||||
|
class="cursor-pointer text-sm text-secondary hover:text-white flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 transition-transform group-open:rotate-90"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Watch video tutorial
|
||||||
|
</summary>
|
||||||
|
<div
|
||||||
|
class="mt-4 rounded-lg overflow-hidden border border-darkless"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
width="100%"
|
||||||
|
height="300"
|
||||||
|
src="https://www.youtube.com/embed/QTwhJy7nT_w?modestbranding=1&rel=0"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if activeSection === "windows"}
|
||||||
|
<div class="bg-dark border border-darkless rounded-xl p-8 shadow-sm">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-2">{sharedTitle}</h3>
|
||||||
|
<p class="text-secondary text-sm">{sharedSubtitle}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-6 h-6 rounded-full bg-darkless text-white flex items-center justify-center text-xs font-bold mt-0.5"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium mb-1">Open PowerShell</p>
|
||||||
|
<p class="text-sm text-secondary">
|
||||||
|
Press <kbd
|
||||||
|
class="bg-darkless text-white px-1.5 py-0.5 rounded text-xs font-mono"
|
||||||
|
>Win+R</kbd
|
||||||
|
>, type <code>powershell</code>, and press Enter.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-6 h-6 rounded-full bg-darkless text-white flex items-center justify-center text-xs font-bold mt-0.5"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<p class="font-medium mb-1">Run the install command</p>
|
||||||
|
<p class="text-sm text-secondary mb-2">
|
||||||
|
Right-click in PowerShell to paste the command.
|
||||||
|
</p>
|
||||||
|
<div class="relative group mt-2">
|
||||||
|
<div
|
||||||
|
class="bg-darker border border-darkless rounded-lg overflow-x-auto"
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
class="p-4 pr-20 text-sm font-mono text-cyan whitespace-pre-wrap break-all"><code
|
||||||
|
>& ([scriptblock]::Create((irm https://hack.club/setup/install.ps1))) -ApiKey {current_user_api_key}</code
|
||||||
|
></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 pt-6 border-t border-darkless">
|
||||||
|
<details class="group">
|
||||||
|
<summary
|
||||||
|
class="cursor-pointer text-sm text-secondary hover:text-white flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 transition-transform group-open:rotate-90"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Watch video tutorial
|
||||||
|
</summary>
|
||||||
|
<div
|
||||||
|
class="mt-4 rounded-lg overflow-hidden border border-darkless"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
width="100%"
|
||||||
|
height="300"
|
||||||
|
src="https://www.youtube.com/embed/fX9tsiRvzhg?modestbranding=1&rel=0"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if activeSection === "advanced"}
|
||||||
|
<div class="bg-dark border border-darkless rounded-xl p-8 shadow-sm">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-2">{sharedTitle}</h3>
|
||||||
|
<p class="text-secondary text-sm">{sharedSubtitle}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-purple/10 border border-purple/20 rounded-lg p-4 mb-4">
|
||||||
|
<p class="text-sm text-purple-200">
|
||||||
|
Create or edit <code
|
||||||
|
class="bg-purple/20 px-1.5 py-0.5 rounded text-white font-mono text-xs"
|
||||||
|
>~/.wakatime.cfg</code
|
||||||
|
> with the following content:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative group">
|
||||||
|
<div
|
||||||
|
class="bg-darker border border-darkless rounded-lg overflow-x-auto"
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
class="p-4 pr-20 text-sm font-mono text-cyan whitespace-pre-wrap break-all"><code
|
||||||
|
>[settings]
|
||||||
|
api_url = {api_url}
|
||||||
|
api_key = {current_user_api_key}
|
||||||
|
heartbeat_rate_limit_seconds = 30</code
|
||||||
|
></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a
|
||||||
|
href="/my/wakatime_setup/step-2"
|
||||||
|
class="text-xs text-secondary hover:text-white transition-colors"
|
||||||
|
>Skip to next step</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
48
app/javascript/pages/WakatimeSetup/Step2.svelte
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Link } from '@inertiajs/svelte';
|
||||||
|
import Stepper from './Stepper.svelte';
|
||||||
|
|
||||||
|
const editors = [
|
||||||
|
{ id: 'vscode', name: 'VS Code', icon: '/images/editor-icons/vs-code-128.png' },
|
||||||
|
{ id: 'vim', name: 'Vim', icon: '/images/editor-icons/vim-128.png' },
|
||||||
|
{ id: 'neovim', name: 'Neovim', icon: '/images/editor-icons/neovim-128.png' },
|
||||||
|
{ id: 'emacs', name: 'Emacs', icon: '/images/editor-icons/emacs-128.png' },
|
||||||
|
{ id: 'pycharm', name: 'PyCharm', icon: '/images/editor-icons/pycharm-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>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Choose your editor - Step 2</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="min-h-screen text-white pt-8 pb-16">
|
||||||
|
<div class="max-w-2xl mx-auto px-4">
|
||||||
|
<Stepper currentStep={2} />
|
||||||
|
|
||||||
|
<div class="text-center mb-10">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Choose your editor</h2>
|
||||||
|
<p class="text-secondary">Select the editor you'll be using. You can set up more later!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||||
|
{#each editors as editor}
|
||||||
|
<a
|
||||||
|
href={`/my/wakatime_setup/step-3?editor=${editor.id}`}
|
||||||
|
class="group flex flex-col items-center justify-center p-6 bg-dark border border-darkless rounded-xl hover:border-primary transition-all duration-200 hover:shadow-lg hover:shadow-primary/10">
|
||||||
|
<div class="w-16 h-16 mb-4 flex items-center justify-center transition-transform duration-200 group-hover:scale-110">
|
||||||
|
{#if editor.icon}
|
||||||
|
<img src={editor.icon} alt={editor.name} class="w-12 h-12 object-contain">
|
||||||
|
{:else}
|
||||||
|
<div class="w-12 h-12 flex items-center justify-center text-3xl">{editor.emoji}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<span class="font-medium text-white group-hover:text-primary transition-colors">{editor.name}</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
483
app/javascript/pages/WakatimeSetup/Step3.svelte
Normal file
|
|
@ -0,0 +1,483 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Link } from "@inertiajs/svelte";
|
||||||
|
import Stepper from "./Stepper.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
current_user_api_key: string;
|
||||||
|
editor: string;
|
||||||
|
heartbeat_check_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { current_user_api_key, editor, heartbeat_check_url }: Props = $props();
|
||||||
|
|
||||||
|
let hasHeartbeat = $state(false);
|
||||||
|
let heartbeatTimeAgo = $state("");
|
||||||
|
let detectedEditor = $state("");
|
||||||
|
let checkCount = $state(0);
|
||||||
|
let statusMessage = $state("Open a file in VS Code and start typing!");
|
||||||
|
let statusPanelClass = $state("border-darkless");
|
||||||
|
let copiedCode = $state("");
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
"Open any code file and start typing!",
|
||||||
|
"Try editing some code in VS Code...",
|
||||||
|
"Type a few characters in your editor!",
|
||||||
|
"We're watching for your first keystroke...",
|
||||||
|
"Make any edit in VS Code to continue!",
|
||||||
|
];
|
||||||
|
|
||||||
|
const editorData: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
methods: Array<{ name: string; code: string; note?: string }>;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
vim: {
|
||||||
|
name: "Vim",
|
||||||
|
icon: "/images/editor-icons/vim-128.png",
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
name: "Using vim-plug",
|
||||||
|
code: "Plug 'wakatime/vim-wakatime'",
|
||||||
|
note: "Then run :PlugInstall",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Using Vundle",
|
||||||
|
code: "Plugin 'wakatime/vim-wakatime'",
|
||||||
|
note: "Then run :PluginInstall",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
neovim: {
|
||||||
|
name: "Neovim",
|
||||||
|
icon: "/images/editor-icons/neovim-128.png",
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
name: "Using lazy.nvim",
|
||||||
|
code: '{ "wakatime/vim-wakatime", lazy = false }',
|
||||||
|
},
|
||||||
|
{ name: "Using packer.nvim", code: "use 'wakatime/vim-wakatime'" },
|
||||||
|
{
|
||||||
|
name: "Using vim-plug",
|
||||||
|
code: "Plug 'wakatime/vim-wakatime'",
|
||||||
|
note: "Then run :PlugInstall",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
emacs: {
|
||||||
|
name: "Emacs",
|
||||||
|
icon: "/images/editor-icons/emacs-128.png",
|
||||||
|
methods: [
|
||||||
|
{
|
||||||
|
name: "Using MELPA",
|
||||||
|
code: "M-x package-install RET wakatime-mode RET",
|
||||||
|
note: "Then add (global-wakatime-mode) to your config.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Using use-package",
|
||||||
|
code: "(use-package wakatime-mode\n :ensure t\n :config\n (global-wakatime-mode))",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function showSuccess(timeAgo: string, editorName: string) {
|
||||||
|
hasHeartbeat = true;
|
||||||
|
heartbeatTimeAgo = timeAgo;
|
||||||
|
detectedEditor = editorName;
|
||||||
|
statusPanelClass = "border-green bg-green/5";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkHeartbeat() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(heartbeat_check_url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${current_user_api_key}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.has_heartbeat) {
|
||||||
|
const heartbeatTime = new Date(data.heartbeat.created_at);
|
||||||
|
const now = new Date();
|
||||||
|
const secondsAgo = (now.getTime() - heartbeatTime.getTime()) / 1000;
|
||||||
|
const recentThreshold = 86400;
|
||||||
|
|
||||||
|
if (secondsAgo <= recentThreshold) {
|
||||||
|
showSuccess(data.time_ago, data.editor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("No recent heartbeats");
|
||||||
|
} catch (error) {
|
||||||
|
checkCount++;
|
||||||
|
|
||||||
|
if (checkCount % 3 === 0) {
|
||||||
|
const msgIndex = Math.floor(checkCount / 3) % messages.length;
|
||||||
|
statusMessage = messages[msgIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (editor === "vscode") {
|
||||||
|
checkHeartbeat();
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (!hasHeartbeat) {
|
||||||
|
checkHeartbeat();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Setup {editor} - Step 3</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="min-h-screen text-white pt-8 pb-16">
|
||||||
|
<div class="max-w-2xl mx-auto px-4">
|
||||||
|
<Stepper currentStep={3} />
|
||||||
|
|
||||||
|
{#if editor === "vscode"}
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="bg-dark border border-darkless rounded-xl p-8 shadow-sm">
|
||||||
|
<div class="flex items-center gap-4 mb-6">
|
||||||
|
<img
|
||||||
|
src="/images/editor-icons/vs-code-128.png"
|
||||||
|
alt="VS Code"
|
||||||
|
class="w-12 h-12 object-contain"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold">
|
||||||
|
Install the VS Code Extension
|
||||||
|
</h3>
|
||||||
|
<p class="text-secondary text-sm">
|
||||||
|
Search for "WakaTime" in the marketplace.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-6 h-6 rounded-full bg-darkless text-white flex items-center justify-center text-xs font-bold mt-0.5"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium mb-1">Install the extension</p>
|
||||||
|
<p class="text-sm text-secondary">
|
||||||
|
Open VS Code, go to Extensions (squares icon), search for <strong
|
||||||
|
>WakaTime</strong
|
||||||
|
>, and click Install.
|
||||||
|
<a
|
||||||
|
href="https://marketplace.visualstudio.com/items?itemName=WakaTime.vscode-wakatime"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-cyan hover:underline">View on Marketplace</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 w-6 h-6 rounded-full bg-darkless text-white flex items-center justify-center text-xs font-bold mt-0.5"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium mb-1">Restart & Code</p>
|
||||||
|
<p class="text-sm text-secondary">
|
||||||
|
Restart VS Code if prompted. Then, open any file and start
|
||||||
|
typing to send your first heartbeat.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4 border-t border-darkless">
|
||||||
|
<details class="group">
|
||||||
|
<summary
|
||||||
|
class="cursor-pointer text-sm text-secondary hover:text-white flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 transition-transform group-open:rotate-90"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
How do I know it's working?
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 pl-6">
|
||||||
|
<p class="text-sm mb-3 text-secondary">
|
||||||
|
You'll see a clock icon in your status bar:
|
||||||
|
</p>
|
||||||
|
<img
|
||||||
|
src="https://hc-cdn.hel1.your-objectstorage.com/s/v3/95d2513ce4b0c1c147827d17ecb4c24540cd73cc_p.png"
|
||||||
|
alt="WakaTime status bar"
|
||||||
|
class="rounded-lg border border-darkless"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border border-darkless rounded-xl p-6 bg-dark transition-all duration-300 {statusPanelClass}"
|
||||||
|
>
|
||||||
|
{#if !hasHeartbeat}
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center text-center py-2"
|
||||||
|
>
|
||||||
|
<h4 class="text-lg font-semibold text-white mb-1">
|
||||||
|
Waiting for you to code...
|
||||||
|
</h4>
|
||||||
|
<p class="text-sm text-secondary mb-4 max-w-sm">
|
||||||
|
{statusMessage}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-center py-2">
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 mx-auto mb-4 rounded-full bg-green/10 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-8 h-8 text-green"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-xl font-bold text-white mb-2">
|
||||||
|
Heartbeat detected!
|
||||||
|
</h4>
|
||||||
|
<p class="text-secondary text-sm mb-6">
|
||||||
|
Received {heartbeatTimeAgo} from {detectedEditor &&
|
||||||
|
detectedEditor.toLowerCase() !== "vscode" &&
|
||||||
|
detectedEditor.toLowerCase() !== "vs code"
|
||||||
|
? detectedEditor
|
||||||
|
: "VS Code"}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/my/wakatime_setup/step-4"
|
||||||
|
class="inline-flex items-center justify-center bg-primary hover:bg-primary/90 text-white px-8 py-3 rounded-lg font-semibold w-full transition-all transform hover:scale-[1.02] active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
Continue →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a
|
||||||
|
href="/my/wakatime_setup/step-4"
|
||||||
|
class="text-xs text-secondary hover:text-white transition-colors"
|
||||||
|
>Skip to finish</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if editor === "godot"}
|
||||||
|
<div class="bg-dark border border-darkless rounded-xl p-8 shadow-sm mb-8">
|
||||||
|
<div class="flex items-center gap-4 mb-6">
|
||||||
|
<img
|
||||||
|
src="/images/editor-icons/godot-128.png"
|
||||||
|
alt="Godot"
|
||||||
|
class="w-12 h-12 object-contain"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold">Godot Setup</h3>
|
||||||
|
<p class="text-secondary text-sm">
|
||||||
|
Install the plugin with your preferred package manager.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm">
|
||||||
|
Godot requires a plugin installed for each project separately.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-darkless/50 rounded-lg p-4">
|
||||||
|
<ol class="list-decimal list-inside space-y-2 text-sm">
|
||||||
|
<li>Open your Godot project</li>
|
||||||
|
<li>Go to <strong>AssetLib</strong> tab</li>
|
||||||
|
<li>Search for <strong>"Godot Super Wakatime"</strong></li>
|
||||||
|
<li>Download and Install</li>
|
||||||
|
<li>
|
||||||
|
Enable in <strong>Project → Project Settings → Plugins</strong>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2">
|
||||||
|
<a
|
||||||
|
href="https://www.youtube.com/watch?v=a938RgsBzNg&t=29s"
|
||||||
|
target="_blank"
|
||||||
|
class="inline-flex items-center gap-2 text-cyan hover:underline text-sm font-medium"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"
|
||||||
|
><path
|
||||||
|
d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
Watch setup tutorial
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/my/wakatime_setup/step-4"
|
||||||
|
class="inline-flex items-center justify-center bg-primary hover:bg-primary/90 text-white px-8 py-3 rounded-lg font-semibold w-full"
|
||||||
|
>
|
||||||
|
Next Step
|
||||||
|
</a>
|
||||||
|
{:else if editorData[editor]}
|
||||||
|
<div class="bg-dark border border-darkless rounded-xl p-8 shadow-sm mb-8">
|
||||||
|
<div class="flex items-center gap-4 mb-6">
|
||||||
|
<img
|
||||||
|
src={editorData[editor].icon}
|
||||||
|
alt={editorData[editor].name}
|
||||||
|
class="w-12 h-12 object-contain"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold">
|
||||||
|
{editorData[editor].name} Setup
|
||||||
|
</h3>
|
||||||
|
<p class="text-secondary text-sm">
|
||||||
|
Install the plugin with your preferred package manager.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
{#each editorData[editor].methods as method, index}
|
||||||
|
{#if index > 0}
|
||||||
|
<div class="pt-6 border-t border-darkless"></div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium mb-2 text-white">{method.name}</h4>
|
||||||
|
<div class="relative group">
|
||||||
|
<div
|
||||||
|
class="bg-darker border border-darkless rounded-lg overflow-x-auto"
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
class="p-4 pr-20 text-sm font-mono text-cyan whitespace-pre"><code
|
||||||
|
>{method.code}</code
|
||||||
|
></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if method.note}
|
||||||
|
<p class="text-xs text-secondary mt-2">{@html method.note}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/my/wakatime_setup/step-4"
|
||||||
|
class="inline-flex items-center justify-center bg-primary hover:bg-primary/90 text-white px-8 py-3 rounded-lg font-semibold w-full"
|
||||||
|
>
|
||||||
|
Next Step
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<div class="bg-dark border border-darkless rounded-xl p-8 shadow-sm mb-8">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-xl font-semibold mb-2">Setup your Editor</h3>
|
||||||
|
<p class="text-secondary text-sm">
|
||||||
|
Install the plugin with your preferred package manager.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm">
|
||||||
|
Find your editor in the WakaTime documentation and follow the
|
||||||
|
installation steps. Use your Hackatime API key when prompted.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-yellow/10 border border-yellow/20 rounded-lg p-4">
|
||||||
|
<p class="text-yellow text-sm font-medium mb-1">⚠️ Important</p>
|
||||||
|
<p class="text-secondary text-sm">
|
||||||
|
Since you already ran the setup script in Step 1, you don't need
|
||||||
|
to configure the <code>api_url</code> or <code>api_key</code> again
|
||||||
|
- just install the plugin!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4 grid grid-cols-2 gap-3">
|
||||||
|
<a
|
||||||
|
href="/docs/editors/pycharm"
|
||||||
|
class="flex items-center gap-3 bg-darkless/50 rounded-lg p-3 hover:bg-darkless transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/editor-icons/pycharm-128.png"
|
||||||
|
alt="PyCharm"
|
||||||
|
class="w-6 h-6"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">PyCharm</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/docs/editors/sublime-text"
|
||||||
|
class="flex items-center gap-3 bg-darkless/50 rounded-lg p-3 hover:bg-darkless transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/editor-icons/sublime-text-128.png"
|
||||||
|
alt="Sublime"
|
||||||
|
class="w-6 h-6"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">Sublime Text</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/docs/editors/unity"
|
||||||
|
class="flex items-center gap-3 bg-darkless/50 rounded-lg p-3 hover:bg-darkless transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/editor-icons/unity-128.png"
|
||||||
|
alt="Unity"
|
||||||
|
class="w-6 h-6"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">Unity</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://wakatime.com/editors"
|
||||||
|
target="_blank"
|
||||||
|
class="flex items-center gap-3 bg-darkless/50 rounded-lg p-3 hover:bg-darkless transition-colors"
|
||||||
|
>
|
||||||
|
<div class="w-6 h-6 flex items-center justify-center">🌐</div>
|
||||||
|
<span class="text-sm">View all editors</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/my/wakatime_setup/step-4"
|
||||||
|
class="inline-flex items-center justify-center bg-primary hover:bg-primary/90 text-white px-8 py-3 rounded-lg font-semibold w-full"
|
||||||
|
>
|
||||||
|
Next Step
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
72
app/javascript/pages/WakatimeSetup/Step4.svelte
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Link } from "@inertiajs/svelte";
|
||||||
|
import Stepper from "./Stepper.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
return_url?: string;
|
||||||
|
return_button_text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { return_url, return_button_text }: Props = $props();
|
||||||
|
|
||||||
|
let agreed = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Setup Complete - Step 4</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="min-h-screen text-white pt-8 pb-16">
|
||||||
|
<div class="max-w-2xl mx-auto px-4">
|
||||||
|
<Stepper currentStep={4} />
|
||||||
|
|
||||||
|
<div class="bg-dark border border-darkless rounded-xl p-6 text-center">
|
||||||
|
<h1 class="text-lg font-bold mb-2">You're all set!</h1>
|
||||||
|
<p class="mb-8 text-sm">
|
||||||
|
Hackatime is configured and tracking your code.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-yellow text-black rounded-xl p-6 mb-8 text-left">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold mb-2">Fair Play Policy</h3>
|
||||||
|
<p class="text-sm mb-3">
|
||||||
|
We have sophisticated measures to detect time manipulation.
|
||||||
|
Attempting to cheat the system will result in a <strong
|
||||||
|
>permanent ban</strong
|
||||||
|
> from Hackatime and all Hack Club events.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm">
|
||||||
|
We are a non-profit running on donations - please respect the
|
||||||
|
community and play fair!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 pt-6 border-t border-yellow/10 flex justify-center">
|
||||||
|
<label
|
||||||
|
class="flex items-center gap-3 cursor-pointer select-none group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={agreed}
|
||||||
|
class="w-5 h-5 rounded border-darkless bg-darker text-primary focus:ring-primary focus:ring-offset-darker transition-colors cursor-pointer"
|
||||||
|
/>
|
||||||
|
<span class="font-medium">I understand and agree to the rules</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="px-8 py-3 bg-primary border border-transparent text-white rounded-lg transition-all font-semibold transform active:scale-[0.98] text-center {agreed
|
||||||
|
? ''
|
||||||
|
: 'opacity-50 cursor-not-allowed pointer-events-none'}"
|
||||||
|
>
|
||||||
|
Let's get going!
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
38
app/javascript/pages/WakatimeSetup/Stepper.svelte
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
currentStep: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { currentStep }: Props = $props();
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ number: 1, label: "Install" },
|
||||||
|
{ number: 2, label: "Editor" },
|
||||||
|
{ number: 3, label: "Plugin" },
|
||||||
|
{ number: 4, label: "Finish" }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="relative flex items-center justify-between w-full max-w-2xl mx-auto">
|
||||||
|
<div class="absolute top-5 left-0 w-full h-0.5 bg-darkless -z-10"></div>
|
||||||
|
|
||||||
|
{#each steps as step}
|
||||||
|
<div class="flex flex-col items-center gap-2 bg-darker z-10 px-2">
|
||||||
|
<div class="w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold border-2 transition-colors duration-200
|
||||||
|
{currentStep > step.number ? 'bg-green border-green text-darker' :
|
||||||
|
currentStep === step.number ? 'bg-primary border-primary text-white' :
|
||||||
|
'bg-dark border-darkless text-secondary'}">
|
||||||
|
{#if currentStep > step.number}
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
{step.number}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-medium {currentStep === step.number ? 'text-white' : 'text-secondary'}">{step.label}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
9
app/javascript/types/globals.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type { FlashData, SharedProps } from '@/types'
|
||||||
|
|
||||||
|
declare module '@inertiajs/core' {
|
||||||
|
export interface InertiaConfig {
|
||||||
|
sharedPageProps: SharedProps
|
||||||
|
flashDataType: FlashData
|
||||||
|
errorValueType: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/javascript/types/index.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
export type FlashData = {
|
||||||
|
notice?: string;
|
||||||
|
alert?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SharedProps = {};
|
||||||
|
|
||||||
|
export type LeaderboardEntryUser = {
|
||||||
|
id: number;
|
||||||
|
display_name: string;
|
||||||
|
avatar_url: string;
|
||||||
|
profile_path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LeaderboardActiveProject = {
|
||||||
|
name: string;
|
||||||
|
repo_url: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LeaderboardEntry = {
|
||||||
|
rank: number;
|
||||||
|
is_current_user: boolean;
|
||||||
|
user: LeaderboardEntryUser;
|
||||||
|
total_seconds: number;
|
||||||
|
total_display: string;
|
||||||
|
streak_count: number;
|
||||||
|
active_project: LeaderboardActiveProject | null;
|
||||||
|
needs_github_link: boolean;
|
||||||
|
settings_path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActivityGraphData = {
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
duration_by_date: Record<string, number>;
|
||||||
|
busiest_day_seconds: number;
|
||||||
|
timezone_label: string;
|
||||||
|
timezone_settings_path: string;
|
||||||
|
};
|
||||||
2
app/javascript/types/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference types="svelte" />
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
@ -11,5 +11,6 @@ class HandleEmailSigninJob < ApplicationJob
|
||||||
|
|
||||||
token = email_address.user.create_email_signin_token(continue_param: continue_param).token
|
token = email_address.user.create_email_signin_token(continue_param: continue_param).token
|
||||||
LoopsMailer.sign_in_email(email_address.email, token).deliver_now
|
LoopsMailer.sign_in_email(email_address.email, token).deliver_now
|
||||||
|
token
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,8 @@ class MigrateUserFromHackatimeJob < ApplicationJob
|
||||||
cursorpos: heartbeat.cursor_position,
|
cursorpos: heartbeat.cursor_position,
|
||||||
project_root_count: heartbeat.project_root_count,
|
project_root_count: heartbeat.project_root_count,
|
||||||
is_write: heartbeat.is_write,
|
is_write: heartbeat.is_write,
|
||||||
source_type: :wakapi_import,
|
source_type: :wakapi_import
|
||||||
raw_data: heartbeat.attributes.slice(*Heartbeat.indexed_attributes)
|
# raw_data: heartbeat.attributes.slice(*Heartbeat.indexed_attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
21
app/models/currently_hacking.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CurrentlyHacking
|
||||||
|
def self.count
|
||||||
|
Cache::CurrentlyHackingCountJob.perform_now[:count]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.active_users
|
||||||
|
data = Cache::CurrentlyHackingJob.perform_now
|
||||||
|
data[:users].map do |user|
|
||||||
|
project = data[:active_projects][user.id]
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
display_name: user.display_name,
|
||||||
|
slack_uid: user.slack_uid,
|
||||||
|
avatar_url: user.avatar_url,
|
||||||
|
active_project: project && { name: project.project_name, repo_url: project.repo_url }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
class Heartbeat < ApplicationRecord
|
class Heartbeat < ApplicationRecord
|
||||||
before_save :set_fields_hash!
|
before_save :set_fields_hash!
|
||||||
before_save :set_raw_data!
|
|
||||||
|
|
||||||
include Heartbeatable
|
include Heartbeatable
|
||||||
include TimeRangeFilterable
|
include TimeRangeFilterable
|
||||||
|
|
@ -112,10 +111,6 @@ class Heartbeat < ApplicationRecord
|
||||||
%w[user_id branch category dependencies editor entity language machine operating_system project type user_agent line_additions line_deletions lineno lines cursorpos project_root_count time is_write]
|
%w[user_id branch category dependencies editor entity language machine operating_system project type user_agent line_additions line_deletions lineno lines cursorpos project_root_count time is_write]
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_raw_data!
|
|
||||||
self.raw_data ||= self.attributes.slice(*self.class.indexed_attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
def soft_delete
|
def soft_delete
|
||||||
update_column(:deleted_at, Time.current)
|
update_column(:deleted_at, Time.current)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
<% content_for :title, "Documentation" %>
|
|
||||||
<% content_for :meta_description, "Complete documentation for Hackatime - learn how to track your coding time and use our API." %>
|
|
||||||
|
|
||||||
<div class="min-h-screen text-white">
|
|
||||||
<div class="max-w-6xl mx-auto px-6 py-8">
|
|
||||||
<h1 class="text-4xl font-bold mb-4">Welcome to <span class="text-primary">Hackatime</span></h1>
|
|
||||||
<p class="text-secondary text-lg mb-8">Tracks your coding time - made by <a href="https://hackclub.com" target="_blank" class="text-primary hover:text-red underline">Hack Club</a></p>
|
|
||||||
|
|
||||||
<div class="bg-dark rounded-lg p-6 mb-8">
|
|
||||||
<p class="mb-4">Hackatime is a free tool from Hack Club. It shows you how much time you spend coding. You can see what other Hack Clubbers are building too!</p>
|
|
||||||
|
|
||||||
<p class="mb-4"><strong class="text-primary">Why we made this:</strong> The more time you spend making things, the better you get at building cool projects!</p>
|
|
||||||
|
|
||||||
<p>Hackatime is totally free. Anyone can see the <a href="https://github.com/hackclub/hackatime" target="_blank" class="text-primary hover:text-red underline">code</a>. It's like <a href="https://wakatime.com" target="_blank" class="text-primary hover:text-red underline">WakaTime</a> but free and open source.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-2xl font-semibold text-primary mb-4">🎯 How to Start</h3>
|
|
||||||
<div class="bg-darkless rounded-lg p-6 mb-8">
|
|
||||||
<ol class="space-y-3 text-lg">
|
|
||||||
<li><strong class="text-white">Make an account</strong> at <a href="<%= root_url %>" class="text-primary hover:text-red underline">hackatime.hackclub.com</a></li>
|
|
||||||
<li><strong class="text-white">Do the setup</strong>: Go to our <a href="<%= my_wakatime_setup_path %>" class="text-primary hover:text-red underline">setup page</a></li>
|
|
||||||
<li><strong class="text-white">Add a plugin</strong>: Put WakaTime in your code editor</li>
|
|
||||||
<li><strong class="text-white">Start coding</strong>: It tracks your time by itself!</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-dark border-l-4 border-primary rounded-r-lg p-6 mb-8"><strong class="text-primary">💡 Tip:</strong> The <a href="<%= my_wakatime_setup_path %>" class="text-primary hover:text-red underline">setup page</a> does everything for you. No hard stuff to figure out!</div>
|
|
||||||
|
|
||||||
<h3 class="text-2xl font-semibold text-primary mb-4">🔌 What Code Editors Work</h3>
|
|
||||||
<div class="bg-dark rounded-lg p-6 mb-8">
|
|
||||||
<p class="mb-6">Hackatime works with <strong class="text-white">any editor</strong> that has <a href="https://wakatime.com" target="_blank" class="text-primary hover:text-red underline">WakaTime</a>. Just add the WakaTime plugin to your editor:</p>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="bg-darkless rounded-lg p-4">
|
|
||||||
<strong class="text-primary text-lg block mb-3">Popular Ones:</strong>
|
|
||||||
<ul class="space-y-2">
|
|
||||||
<li><%= link_to "VS Code", doc_path("editors/vs-code"), class: "text-white hover:text-primary" %></li>
|
|
||||||
<li><%= link_to "PyCharm", doc_path("editors/pycharm"), class: "text-white hover:text-primary" %></li>
|
|
||||||
<li><%= link_to "Vim", doc_path("editors/vim"), class: "text-white hover:text-primary" %></li>
|
|
||||||
<li><%= link_to "Sublime Text", doc_path("editors/sublime-text"), class: "text-white hover:text-primary" %></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="bg-darkless rounded-lg p-4">
|
|
||||||
<strong class="text-primary text-lg block mb-3">Lots More:</strong>
|
|
||||||
<ul class="space-y-2">
|
|
||||||
<li><%= link_to "Brackets", doc_path("editors/brackets"), class: "text-white hover:text-primary" %></li>
|
|
||||||
<li class="text-secondary">All JetBrains apps</li>
|
|
||||||
<li><%= link_to "Command Line", doc_path("editors/terminal"), class: "text-white hover:text-primary" %></li>
|
|
||||||
<li><a href="#supported-editors" onclick="document.getElementById('all-editors').parentElement.open = true; document.getElementById('all-editors').scrollIntoView({behavior: 'smooth'});" class="text-primary hover:text-red">70+ others</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-2xl font-semibold text-primary mb-4">📚 Help Pages</h3>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
||||||
<div class="bg-dark rounded-lg p-6">
|
|
||||||
<h4 class="text-xl font-semibold text-primary mb-4">Getting Started</h4>
|
|
||||||
<ul class="space-y-3">
|
|
||||||
<li><%= link_to "Quick Start Guide", doc_path("getting-started/quick-start"), class: "text-white hover:text-primary" %> <span class="text-secondary">- Start in 5 minutes</span></li>
|
|
||||||
<li><%= link_to "Installation", doc_path("getting-started/installation"), class: "text-white hover:text-primary" %> <span class="text-secondary">- Add plugins to your editor</span></li>
|
|
||||||
<li><%= link_to "Configuration", doc_path("getting-started/configuration"), class: "text-white hover:text-primary" %> <span class="text-secondary">- Advanced setup stuff</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-dark rounded-lg p-6">
|
|
||||||
<h4 class="text-xl font-semibold text-primary mb-4">For Coders</h4>
|
|
||||||
<ul class="space-y-3">
|
|
||||||
<li><a href="/api-docs" class="text-white hover:text-primary">API Documentation</a> <span class="text-secondary">- Interactive API docs with Swagger</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-2xl font-semibold text-primary mb-4">🔒 Safe & Free</h3>
|
|
||||||
<div class="bg-dark rounded-lg p-6 mb-8">
|
|
||||||
<ul class="space-y-3">
|
|
||||||
<li><strong class="text-primary">Open Source</strong>: You can see all the code on <a href="https://github.com/hackclub/hackatime" target="_blank" class="text-primary hover:text-red underline">GitHub</a></li>
|
|
||||||
<li><strong class="text-primary">Works Offline</strong>: Time tracking continues even without internet - your data syncs when you reconnect</li>
|
|
||||||
<li><strong class="text-primary">Private</strong>: We only see how long you code, not what you type</li>
|
|
||||||
<li><strong class="text-primary">Community Made</strong>: Built by Hack Club</li>
|
|
||||||
<li><strong class="text-primary">Free</strong>: You can donate to cover server costs at <a href="https://hackclub.com/donate" target="_blank" class="text-primary hover:text-red underline">hackclub.com/donate</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-2xl font-semibold text-primary mb-4">🆘 Need Help?</h3>
|
|
||||||
<div class="bg-darkless rounded-lg p-6 mb-8">
|
|
||||||
<p class="mb-4">
|
|
||||||
<strong class="text-primary">Not seeing your time?</strong> Go to the <a href="<%= my_wakatime_setup_path %>" class="text-primary hover:text-red underline">setup page</a>
|
|
||||||
to check if everything is working.
|
|
||||||
</p>
|
|
||||||
<p><strong class="text-primary">Have questions?</strong> Ask in the <a href="https://hackclub.slack.com" target="_blank" class="text-primary hover:text-red underline">Hack Club Slack</a> (#hackatime-v2 channel) or <a href="https://github.com/hackclub/hackatime/issues" target="_blank" class="text-primary hover:text-red underline">ask on GitHub</a>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-2xl font-semibold text-primary mb-4">🔧 Supported Editors</h3>
|
|
||||||
<div class="bg-dark rounded-lg p-6 mb-8">
|
|
||||||
<p class="mb-6">Hackatime works with any editor that supports WakaTime. Click on your editor below for setup instructions:</p>
|
|
||||||
|
|
||||||
<div id="supported-editors" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
||||||
<!-- Most popular editors among teenagers, ordered by popularity -->
|
|
||||||
<% popular_editors = [['VS Code', 'vs-code'], %w[PyCharm pycharm], ['IntelliJ IDEA', 'intellij-idea'], ['Sublime Text', 'sublime-text'], %w[Vim vim], %w[Neovim neovim], ['Android Studio', 'android-studio'], %w[Xcode xcode], %w[Unity unity], %w[Godot godot], %w[Cursor cursor], %w[Zed zed], %w[Terminal terminal], %w[WebStorm webstorm], %w[Eclipse eclipse], %w[Emacs emacs], %w[Jupyter jupyter], %w[OnShape onshape]] %>
|
|
||||||
<% popular_editors.each do |name, slug| %>
|
|
||||||
<a href="<%= doc_path("editors/#{slug}") %>" class="bg-darkless rounded-lg p-3 hover:bg-primary/75 transition-colors text-center block">
|
|
||||||
<img src="/images/editor-icons/<%= slug %>-128.png" alt="<%= name %>" class="w-12 h-12 mx-auto mb-2">
|
|
||||||
<div class="text-sm text-white"><%= name %></div>
|
|
||||||
</a>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<details class="bg-dark rounded-lg p-6 mb-8">
|
|
||||||
<summary class="cursor-pointer font-semibold text-primary">View all 80 supported editors</summary>
|
|
||||||
<div id="all-editors" class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-3 mt-4 p-4">
|
|
||||||
<%
|
|
||||||
# All 80 editors - alphabetically sorted
|
|
||||||
all_editors = [['Android Studio', 'android-studio'], %w[AppCode appcode], %w[Aptana aptana], ['Arduino IDE', 'arduino-ide'], ['Azure Data Studio', 'azure-data-studio'], %w[Blender blender], %w[Brackets brackets], %w[Brave brave], ['C++ Builder', 'c++-builder'], %w[Canva canva], %w[Chrome chrome], %w[CLion clion], %w[Cloud9 cloud9], %w[Coda coda], %w[CodeTasty codetasty], %w[Cursor cursor], %w[DataGrip datagrip], %w[DataSpell dataspell], %w[DBeaver dbeaver], %w[Delphi delphi], %w[Discord discord], %w[Eclipse eclipse], %w[Edge edge], %w[Emacs emacs], %w[Eric eric], %w[Excel excel], %w[Figma figma], %w[Firefox firefox], %w[Gedit gedit], %w[Godot godot], %w[GoLand goland], ['HBuilder X', 'hbuilder-x'], ['IDA Pro', 'ida-pro'], ['IntelliJ IDEA', 'intellij-idea'], %w[Jupyter jupyter], %w[Kakoune kakoune], %w[Kate kate], %w[Komodo komodo], %w[Micro micro], %w[MPS mps], %w[Neovim neovim], %w[NetBeans netbeans], %w[Notepad++ notepad++], %w[Nova nova], %w[Obsidian obsidian], %w[OnShape onshape], %w[Oxygen oxygen], %w[PhpStorm phpstorm], %w[Postman postman], %w[PowerPoint powerpoint], %w[Processing processing], %w[Pulsar pulsar], %w[PyCharm pycharm], %w[ReClassEx reclassex], %w[Rider rider], ['Roblox Studio', 'roblox-studio'], %w[RubyMine rubymine], %w[RustRover rustrover], %w[Safari safari], %w[SiYuan siyuan], %w[Sketch sketch], %w[SlickEdit slickedit], ['SQL Server Management Studio', 'sql-server-management-studio'], ['Sublime Text', 'sublime-text'], %w[Terminal terminal], %w[TeXstudio texstudio], %w[TextMate textmate], %w[Trae trae], %w[Unity unity], ['Unreal Engine 4', 'unreal-engine-4'], %w[Vim vim], ['Visual Studio', 'visual-studio'], ['VS Code', 'vs-code'], %w[WebStorm webstorm], %w[Windsurf windsurf], %w[Wing wing], %w[Word word], %w[Xcode xcode], %w[Zed zed], ['Swift Playgrounds', 'swift-playgrounds']].sort_by { |editor| editor[0] }
|
|
||||||
%>
|
|
||||||
<% all_editors.each do |name, slug| %>
|
|
||||||
<a href="<%= doc_path("editors/#{slug}") %>" class="bg-darkless rounded p-2 hover:bg-primary/75 transition-colors text-center block">
|
|
||||||
<img src="/images/editor-icons/<%= slug %>-128.png" alt="<%= name %>" class="w-8 h-8 mx-auto mb-1">
|
|
||||||
<div class="text-xs text-white leading-tight"><%= name %></div>
|
|
||||||
</a>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div class="border-t border-darkless my-8"></div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<p class="text-secondary">
|
|
||||||
Found an issue with the docs?
|
|
||||||
<a href="https://github.com/hackclub/hackatime" target="_blank" class="text-primary hover:text-red underline"> Edit on GitHub </a> - we'd love your help making them better!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
<% content_for :title, "#{@title} - Hackatime Documentation" %>
|
|
||||||
<% content_for :meta_description, generate_doc_description(@content, @title) %>
|
|
||||||
<% content_for :meta_keywords, generate_doc_keywords(@doc_path, @title) %>
|
|
||||||
<% content_for :canonical_url, doc_url(@doc_path) %>
|
|
||||||
<% content_for :og_type, "article" %>
|
|
||||||
<% content_for :og_title, "#{@title} - Hackatime Documentation" %>
|
|
||||||
|
|
||||||
<!-- JSON-LD Structured Data -->
|
|
||||||
<% content_for :head do %>
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "TechArticle",
|
|
||||||
"headline": <%== @title.to_json %>,
|
|
||||||
"description": <%== generate_doc_description(@content, @title).to_json %>,
|
|
||||||
"url": <%== doc_url(@doc_path).to_json %>,
|
|
||||||
"dateModified": <%== (File.mtime(safe_docs_path("#{@doc_path}.md")).iso8601 rescue Time.current.iso8601).to_json %>,
|
|
||||||
"author": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "Hack Club",
|
|
||||||
"url": "https://hackclub.com"
|
|
||||||
},
|
|
||||||
"publisher": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "Hack Club",
|
|
||||||
"url": "https://hackclub.com"
|
|
||||||
},
|
|
||||||
"mainEntityOfPage": {
|
|
||||||
"@type": "WebPage",
|
|
||||||
"@id": <%== doc_url(@doc_path).to_json %>
|
|
||||||
},
|
|
||||||
"about": {
|
|
||||||
"@type": "SoftwareApplication",
|
|
||||||
"name": "Hackatime",
|
|
||||||
"description": "Free and open source time tracking alternative to WakaTime"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
.prose a {
|
|
||||||
color: var(--color-primary) !important;
|
|
||||||
text-decoration: underline !important;
|
|
||||||
font-weight: 500 !important;
|
|
||||||
}
|
|
||||||
.prose a:hover {
|
|
||||||
color: var(--color-red) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="min-h-screen text-white">
|
|
||||||
<div class="max-w-6xl mx-auto px-6 py-8">
|
|
||||||
<nav class="mb-8">
|
|
||||||
<% @breadcrumbs.each_with_index do |crumb, index| %>
|
|
||||||
<% if index == @breadcrumbs.length - 1 %>
|
|
||||||
<span class="text-primary"><%= crumb[:name] %></span>
|
|
||||||
<% else %>
|
|
||||||
<% if crumb[:is_link] && crumb[:path] %>
|
|
||||||
<%= link_to crumb[:name], crumb[:path], class: "text-secondary hover:text-primary" %>
|
|
||||||
<% else %>
|
|
||||||
<span class="text-secondary"><%= crumb[:name] %></span>
|
|
||||||
<% end %>
|
|
||||||
<span class="text-secondary mx-2">/</span>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</nav>
|
|
||||||
<div
|
|
||||||
class="bg-dark rounded-lg p-8 mb-8 prose prose-invert prose-lg max-w-none
|
|
||||||
prose-headings:text-primary prose-headings:font-bold prose-headings:leading-tight
|
|
||||||
prose-h1:text-4xl prose-h1:mb-6 prose-h1:text-primary prose-h1:mt-0
|
|
||||||
prose-h2:text-2xl prose-h2:mt-10 prose-h2:mb-4 prose-h2:text-primary prose-h2:border-b prose-h2:border-primary/20 prose-h2:pb-2
|
|
||||||
prose-h3:text-xl prose-h3:mt-8 prose-h3:mb-3 prose-h3:text-primary
|
|
||||||
prose-h4:text-lg prose-h4:mt-4 prose-h4:mb-2 prose-h4:text-white prose-h4:font-semibold
|
|
||||||
prose-p:text-white prose-p:leading-7 prose-p:mb-5
|
|
||||||
prose-a:text-primary prose-a:hover:text-red prose-a:underline prose-a:font-medium
|
|
||||||
prose-strong:text-white prose-strong:font-semibold
|
|
||||||
prose-em:text-secondary prose-em:italic
|
|
||||||
prose-code:bg-darkless prose-code:text-primary prose-code:px-2 prose-code:py-1 prose-code:rounded prose-code:text-sm prose-code:font-mono
|
|
||||||
prose-pre:bg-darkless prose-pre:border prose-pre:border-primary/20 prose-pre:rounded-lg prose-pre:p-4 prose-pre:overflow-x-auto
|
|
||||||
prose-pre:text-white prose-pre:text-sm
|
|
||||||
prose-blockquote:border-l-4 prose-blockquote:border-primary prose-blockquote:bg-darkless prose-blockquote:pl-6 prose-blockquote:py-4 prose-blockquote:rounded-r-lg
|
|
||||||
prose-blockquote:text-secondary prose-blockquote:italic prose-blockquote:font-normal prose-blockquote:my-6
|
|
||||||
prose-ul:text-white prose-ul:mb-4 prose-ul:pl-6
|
|
||||||
prose-ol:text-white prose-ol:mb-4 prose-ol:pl-6
|
|
||||||
prose-li:text-white prose-li:mb-3 prose-li:leading-7 prose-li:pl-2
|
|
||||||
prose-table:border-collapse prose-table:border prose-table:border-primary/20 prose-table:rounded-lg prose-table:overflow-hidden prose-table:my-6
|
|
||||||
prose-th:bg-darkless prose-th:text-primary prose-th:font-semibold prose-th:p-3 prose-th:border prose-th:border-primary/20
|
|
||||||
prose-td:text-white prose-td:p-3 prose-td:border prose-td:border-primary/20
|
|
||||||
prose-img:rounded-lg prose-img:shadow-lg prose-img:mx-auto prose-img:block prose-img:max-w-24 prose-img:h-auto prose-img:my-4
|
|
||||||
prose-hr:border-primary/20 prose-hr:my-8
|
|
||||||
[&_ol>li::marker]:text-primary [&_ol>li::marker]:font-semibold
|
|
||||||
[&_ul>li::marker]:text-primary
|
|
||||||
[&_ol>li]:mb-3 [&_ol>li]:pl-2
|
|
||||||
[&_h2:not(:first-child)]:mt-10
|
|
||||||
[&_h3:not(:first-child)]:mt-8
|
|
||||||
[&_p_strong:first-child]:text-primary
|
|
||||||
[&_pre[class*='language-json']]:bg-darkless [&_pre[class*='language-json']]:border [&_pre[class*='language-json']]:border-primary/10
|
|
||||||
[&_pre[class*='language-bash']]:bg-darkless [&_pre[class*='language-bash']]:border [&_pre[class*='language-bash']]:border-primary/10
|
|
||||||
[&_img[alt*='PyCharm']]:w-16 [&_img[alt*='PyCharm']]:h-16 [&_img[alt*='PyCharm']]:mx-auto [&_img[alt*='PyCharm']]:block [&_img[alt*='PyCharm']]:my-4
|
|
||||||
[&_img[alt*='VS_Code']]:w-16 [&_img[alt*='VS_Code']]:h-16 [&_img[alt*='VS_Code']]:mx-auto [&_img[alt*='VS_Code']]:block [&_img[alt*='VS_Code']]:my-4
|
|
||||||
[&_img[alt*='IntelliJ']]:w-16 [&_img[alt*='IntelliJ']]:h-16 [&_img[alt*='IntelliJ']]:mx-auto [&_img[alt*='IntelliJ']]:block [&_img[alt*='IntelliJ']]:my-4
|
|
||||||
[&_img[src*='/images/editor-icons/']]:w-16 [&_img[src*='/images/editor-icons/']]:h-16 [&_img[src*='/images/editor-icons/']]:mx-auto [&_img[src*='/images/editor-icons/']]:block [&_img[src*='/images/editor-icons/']]:my-4
|
|
||||||
[&_.editor-steps]:bg-darkless [&_.editor-steps]:p-6 [&_.editor-steps]:rounded-lg [&_.editor-steps]:my-4
|
|
||||||
[&_.editor-steps_ol]:m-0
|
|
||||||
[&_.editor-steps_li]:mb-2">
|
|
||||||
<%= sanitize @rendered_content, tags: %w[h1 h2 h3 h4 h5 h6 p a ul ol li code pre strong em img div span table thead tbody tr th td hr br blockquote], attributes: %w[href src alt title class id] %>
|
|
||||||
</div>
|
|
||||||
<div class="text-center bg-darkless rounded-lg p-6">
|
|
||||||
<p class="text-secondary">
|
|
||||||
Found an issue with this page?
|
|
||||||
<a href="https://github.com/hackclub/hackatime/edit/main/docs/<%= @doc_path %>.md" target="_blank" class="text-primary hover:text-red underline">Edit it on GitHub</a> - we'd love your help making the docs better!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<div>
|
|
||||||
<h1 class="font-bold text-4xl">Extensions</h1>
|
|
||||||
<em class="m-4">These are third-party extensions that can be used with Hackatime. Everything in the list is community made and not guaranteed to work!</em>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
<div class="border rounded-lg p-4 bg-darkless">
|
|
||||||
<h2 class="font-bold text-2xl mb-2">Hackatime Desktop</h2>
|
|
||||||
<p>Desktop app for Hackatime. Runs on Mac, Windows, and Linux.</p>
|
|
||||||
<%= link_to 'Source', 'https://github.com/hackclub/hackatime-desktop', class: 'text-primary hover:underline', allow_host: true %> |
|
|
||||||
<%= link_to 'Install', 'https://github.com/hackclub/hackatime-desktop/releases', class: 'text-primary hover:underline', allow_host: true %>
|
|
||||||
</div>
|
|
||||||
<div class="border rounded-lg p-4 bg-darkless">
|
|
||||||
<h2 class="font-bold text-2xl mb-2">Cattatime</h2>
|
|
||||||
<p>A Tamagotchi system for Hackatime. Code, fill your cup, and get your pet rewards. Available for windows and mac.</p>
|
|
||||||
<%= link_to 'Source', 'https://github.com/joysudo/catatime/tree/master', class: 'text-primary hover:underline', allow_host: true %> |
|
|
||||||
<%= link_to 'Install', 'https://github.com/joysudo/catatime/releases/', class: 'text-primary hover:underline', allow_host: true %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -15,6 +15,9 @@
|
||||||
<link rel="canonical" href="<%= content_for(:canonical_url) || request.original_url %>">
|
<link rel="canonical" href="<%= content_for(:canonical_url) || request.original_url %>">
|
||||||
|
|
||||||
<!-- Enhanced SEO -->
|
<!-- Enhanced SEO -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<meta name="theme-color" content="#ec3750">
|
<meta name="theme-color" content="#ec3750">
|
||||||
<meta name="msapplication-TileColor" content="#ec3750">
|
<meta name="msapplication-TileColor" content="#ec3750">
|
||||||
|
|
||||||
|
|
@ -163,6 +166,21 @@
|
||||||
<% if Sentry.get_trace_propagation_meta %>
|
<% if Sentry.get_trace_propagation_meta %>
|
||||||
<%= sanitize Sentry.get_trace_propagation_meta, tags: %w[meta], attributes: %w[name content] %>
|
<%= sanitize Sentry.get_trace_propagation_meta, tags: %w[meta], attributes: %w[name content] %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= vite_stylesheet_tag "application" %>
|
||||||
|
<%= vite_client_tag %>
|
||||||
|
<%= vite_typescript_tag "inertia" %>
|
||||||
|
<%= inertia_ssr_head %>
|
||||||
|
<%= vite_typescript_tag 'application' %>
|
||||||
|
<!--
|
||||||
|
If using a TypeScript entrypoint file:
|
||||||
|
vite_typescript_tag 'application'
|
||||||
|
|
||||||
|
If using a .jsx or .tsx entrypoint, add the extension:
|
||||||
|
vite_javascript_tag 'application.jsx'
|
||||||
|
|
||||||
|
Visit the guide for more information: https://vite-ruby.netlify.app/guide/rails
|
||||||
|
-->
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="<%= content_for(:body_class) %> flex min-h-screen bg-darker" data-controller="nav">
|
<body class="<%= content_for(:body_class) %> flex min-h-screen bg-darker" data-controller="nav">
|
||||||
|
|
@ -194,12 +212,13 @@
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="fixed top-0 right-5 max-w-sm max-h-[80vh] bg-elevated border border-darkless rounded-b-xl shadow-lg z-1000 overflow-hidden transform -translate-y-full transition-transform duration-300 ease-out hidden" data-controller="currently-hacking" data-currently-hacking-target="container" data-currently-hacking-count-url-value="<%= currently_hacking_count_static_pages_path %>" data-currently-hacking-full-url-value="<%= currently_hacking_static_pages_path %>" data-currently-hacking-interval-value="60000">
|
<% currently_hacking_count = CurrentlyHacking.count %>
|
||||||
<div class="currently-hacking p-3 bg-elevated cursor-pointer select-none bg-dark flex items-center justify-between">
|
<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 translate-y-0 transition-transform duration-300 ease-out" data-controller="currently-hacking" data-currently-hacking-target="container" data-currently-hacking-count-url-value="<%= currently_hacking_count_static_pages_path %>" data-currently-hacking-full-url-value="<%= currently_hacking_static_pages_path %>" data-currently-hacking-interval-value="60000">
|
||||||
|
<div class="currently-hacking p-3 bg-dark cursor-pointer select-none flex items-center justify-between">
|
||||||
<div class="text-white text-sm font-medium">
|
<div class="text-white text-sm font-medium">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-2 h-2 rounded-full bg-green-500 animate-pulse mr-2"></div>
|
<div class="w-2 h-2 rounded-full bg-green-500 animate-pulse mr-2"></div>
|
||||||
<span data-currently-hacking-target="count" class="text-lg"></span>
|
<span data-currently-hacking-target="count" class="text-lg"><%= pluralize(currently_hacking_count, "person") %> currently hacking</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
158
app/views/layouts/inertia.html.erb
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html class="<%= Rails.env == 'production' ? 'production' : 'development' %>" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<title><%= @page_title || content_for(:title) || 'Hackatime' %></title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="color-scheme" content="dark">
|
||||||
|
<meta name="description" content="<%= @meta_description || content_for(:meta_description) || 'Free and open-source coding time tracker built by Hack Club. Track your time across 75+ editors.' %>">
|
||||||
|
<meta name="keywords" content="<%= @meta_keywords || content_for(:meta_keywords) || 'coding time tracker, programming stats, wakatime alternative, free time tracking, code statistics, developer analytics, programming time, coding productivity' %>">
|
||||||
|
<meta name="author" content="Hack Club">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
<link rel="canonical" href="<%= content_for(:canonical_url) || request.original_url %>">
|
||||||
|
<meta name="theme-color" content="#ec3750">
|
||||||
|
<meta name="msapplication-TileColor" content="#ec3750">
|
||||||
|
<meta property="og:title" content="<%= @og_title || content_for(:og_title) || @page_title || content_for(:title) || 'Hackatime - Free Coding Time Tracker' %>">
|
||||||
|
<meta property="og:description" content="<%= @og_description || content_for(:og_description) || @meta_description || content_for(:meta_description) || 'Free and open-source coding time tracker built by Hack Club. Track your time across 75+ editors.' %>">
|
||||||
|
<meta property="og:url" content="<%= content_for(:og_url) || request.original_url %>">
|
||||||
|
<meta property="og:type" content="<%= content_for(:og_type) || 'website' %>">
|
||||||
|
<meta property="og:image" content="<%= content_for(:og_image) || asset_path('favicon.png') %>">
|
||||||
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
<meta property="og:site_name" content="Hackatime">
|
||||||
|
<meta property="og:locale" content="en_US">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:site" content="@hackclub">
|
||||||
|
<meta name="twitter:creator" content="@hackclub">
|
||||||
|
<meta name="twitter:title" content="<%= @twitter_title || content_for(:twitter_title) || @page_title || content_for(:title) || 'Hackatime - Free Coding Time Tracker' %>">
|
||||||
|
<meta name="twitter:description" content="<%= @twitter_description || content_for(:twitter_description) || @meta_description || content_for(:meta_description) || 'Free and open-source coding time tracker built by Hack Club. Track your time across 75+ editors.' %>">
|
||||||
|
<meta name="twitter:image" content="<%= content_for(:twitter_image) || asset_path('favicon.png') %>">
|
||||||
|
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
|
<% if current_user %>
|
||||||
|
<meta name="user-is-superadmin" content="<%= current_user.admin_level == 'superadmin' %>">
|
||||||
|
<meta name="user-is-admin" content="<%= current_user.admin_level == 'admin' %>">
|
||||||
|
<meta name="user-is-viewer" content="<%= current_user.admin_level == 'viewer' %>">
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= yield :head %>
|
||||||
|
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "SoftwareApplication",
|
||||||
|
"name": "Hackatime",
|
||||||
|
"alternateName": "Hack Club Hackatime",
|
||||||
|
"applicationCategory": "DeveloperApplication",
|
||||||
|
"operatingSystem": "Any",
|
||||||
|
"description": "Track your coding time easily with Hackatime. A free tool to see how much time you spend programming in different languages and editors.",
|
||||||
|
"url": "https://hackatime.hackclub.com",
|
||||||
|
"downloadUrl": "https://hackatime.hackclub.com",
|
||||||
|
"sameAs": ["https://github.com/hackclub/hackatime", "https://hackatime.hackclub.com/docs"],
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "USD",
|
||||||
|
"availability": "https://schema.org/InStock"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Hack Club",
|
||||||
|
"url": "https://hackclub.com"
|
||||||
|
},
|
||||||
|
"softwareVersion": "2.0",
|
||||||
|
"datePublished": "2025-01-01",
|
||||||
|
"license": "https://opensource.org/licenses/MIT",
|
||||||
|
"programmingLanguage": "Ruby",
|
||||||
|
"codeRepository": "https://github.com/hackclub/hackatime",
|
||||||
|
"supportingData": "Free coding time tracker",
|
||||||
|
"featureList": ["Track coding time across 75+ editors", "See which languages you use most", "View daily coding statistics", "Compare with other high schoolers", "Free and open source"]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Hack Club",
|
||||||
|
"url": "https://hackclub.com",
|
||||||
|
"logo": "https://hackclub.com/logo.png",
|
||||||
|
"sameAs": ["https://twitter.com/hackclub", "https://github.com/hackclub"]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "Hackatime",
|
||||||
|
"alternateName": "Hack Club Hackatime",
|
||||||
|
"url": "https://hackatime.hackclub.com"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<% if request.path == "/" %>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "FAQPage",
|
||||||
|
"mainEntity": [
|
||||||
|
{
|
||||||
|
"@type": "Question",
|
||||||
|
"name": "What is Hackatime?",
|
||||||
|
"acceptedAnswer": {
|
||||||
|
"@type": "Answer",
|
||||||
|
"text": "Hackatime is a free coding time tracker that helps you see how much time you spend programming. It tracks your coding time across different languages and editors."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Question",
|
||||||
|
"name": "Is Hackatime free?",
|
||||||
|
"acceptedAnswer": {
|
||||||
|
"@type": "Answer",
|
||||||
|
"text": "Yes! Hackatime is completely free to use. There are no paid plans or hidden costs."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Question",
|
||||||
|
"name": "How is Hackatime different from WakaTime?",
|
||||||
|
"acceptedAnswer": {
|
||||||
|
"@type": "Answer",
|
||||||
|
"text": "Hackatime is free and open source, while WakaTime has paid plans. Hackatime gives you all features for free and you can host it yourself."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Question",
|
||||||
|
"name": "Is Hackatime the same as WakaTime?",
|
||||||
|
"acceptedAnswer": {
|
||||||
|
"@type": "Answer",
|
||||||
|
"text": "No. Hackatime is a separate, independent open-source project built by Hack Club. While both track coding time, Hackatime is completely free and designed for high school students in the Hack Club community."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= favicon_link_tag asset_path('favicon.png'), type: 'image/png' %>
|
||||||
|
|
||||||
|
<script defer data-domain="hackatime.hackclub.com" src="https://plausible.io/js/script.file-downloads.hash.js"></script>
|
||||||
|
|
||||||
|
<%= stylesheet_link_tag :app %>
|
||||||
|
<%= stylesheet_link_tag 'tailwind', 'data-turbo-track': 'reload' %>
|
||||||
|
<% if Sentry.get_trace_propagation_meta %>
|
||||||
|
<%= sanitize Sentry.get_trace_propagation_meta, tags: %w[meta], attributes: %w[name content] %>
|
||||||
|
<% end %>
|
||||||
|
<%= vite_stylesheet_tag "application" %>
|
||||||
|
<%= vite_client_tag %>
|
||||||
|
<%= vite_typescript_tag "inertia" %>
|
||||||
|
<%= inertia_ssr_head %>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="<%= content_for(:body_class) %> flex min-h-screen bg-darker">
|
||||||
|
<%= yield %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<aside class="flex flex-col min-h-screen w-[250px] bg-dark text-white px-2 py-4 rounded-r-lg overflow-y-auto lg:block" data-nav-target="nav" style="scrollbar-width: none; -ms-overflow-style: none;">
|
<aside class="flex flex-col min-h-screen w-[250px] bg-dark text-white px-3 py-4 rounded-r-lg overflow-y-auto lg:block" data-nav-target="nav" style="scrollbar-width: none; -ms-overflow-style: none;">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<% flash.each do |name, msg| %>
|
<% flash.each do |name, msg| %>
|
||||||
<%
|
<%
|
||||||
|
|
@ -10,12 +10,11 @@
|
||||||
'border-primary text-primary'
|
'border-primary text-primary'
|
||||||
end
|
end
|
||||||
%>
|
%>
|
||||||
<div>
|
<div class="rounded-md text-center text-sm px-3 py-2 <%= c %>"><%= msg %></div>
|
||||||
<div class="rounded-lg border text-center text-lg px-3 py-2 mb-2 <%= c %>"><%= msg %></div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if current_user %>
|
<% if current_user %>
|
||||||
<div class="px-2 rounded-lg flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2 pb-3 border-b border-darkless">
|
||||||
<%= render 'shared/user_mention', user: current_user %>
|
<%= render 'shared/user_mention', user: current_user %>
|
||||||
<%= render 'static_pages/streak', user: current_user, show_text: true, turbo_frame: false %>
|
<%= render 'static_pages/streak', user: current_user, show_text: true, turbo_frame: false %>
|
||||||
<% if current_user.admin_level != 0 %>
|
<% if current_user.admin_level != 0 %>
|
||||||
|
|
@ -23,152 +22,177 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="mb-1">
|
<div>
|
||||||
<%= link_to 'Login', slack_auth_path, class: 'block px-2 py-1 rounded-lg transition text-white font-bold bg-primary hover:bg-secondary text-lg' %>
|
<%= link_to 'Login', slack_auth_path, class: 'block px-4 py-2 rounded-md transition text-white font-semibold bg-primary hover:bg-secondary text-center' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
|
||||||
<div class="space-y-1 text-lg">
|
|
||||||
<div>
|
|
||||||
<%= link_to root_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(root_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Home
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<%= link_to leaderboards_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(leaderboards_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Leaderboards
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% unless current_user %>
|
|
||||||
<div>
|
|
||||||
<%= link_to docs_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(docs_path) || request.path.start_with?('/docs') ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Docs
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<%= link_to extensions_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(extensions_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Extensions
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<%= link_to "/what-is-hackatime", class: "block px-2 py-1 rounded-lg transition #{current_page?('/what-is-hackatime') ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
What is Hackatime?
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if current_user %>
|
|
||||||
<div>
|
|
||||||
<%= link_to my_projects_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(my_projects_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Projects
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<%= link_to docs_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(docs_path) || request.path.start_with?('/docs') ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Docs
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<%= link_to extensions_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(extensions_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Extensions
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<%= link_to my_settings_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(my_settings_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Settings
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<%= link_to oauth_applications_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(oauth_applications_path) || request.path.start_with?('/oauth/applications') ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
My OAuth Apps
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button type="button" onclick="showLogout()" class="w-full text-left cursor-pointer block px-[15px] py-2.5 rounded-lg transition hover:text-primary hover:bg-darkless">Logout</button>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% dev_tool(nil, "div") do %>
|
|
||||||
<%= link_to letter_opener_web_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(letter_opener_web_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Letter Opener
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% dev_tool(nil, "div") do %>
|
|
||||||
<%= link_to '/rails/mailers', class: "block px-2 py-1 rounded-lg transition #{current_page?('/rails/mailers') ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Mailers
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% if current_user&.admin_level == "admin" || current_user&.admin_level == "superadmin" %>
|
|
||||||
<% admin_tool(nil, "div") do %>
|
|
||||||
<%= link_to admin_timeline_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(admin_timeline_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Review Timeline
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% elsif current_user&.admin_level == "viewer" %>
|
|
||||||
<% viewer_tool(nil, "div") do %>
|
|
||||||
<%= link_to admin_timeline_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(admin_timeline_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Review Timeline
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if current_user&.admin_level == "admin" || current_user&.admin_level == "superadmin" %>
|
<nav class="space-y-1">
|
||||||
<% admin_tool(nil, "div") do %>
|
<div>
|
||||||
<%= link_to admin_trust_level_audit_logs_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(admin_trust_level_audit_logs_path) || request.path.start_with?('/admin/trust_level_audit_logs') ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
<%= link_to root_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(root_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
Trust Level Logs
|
Home
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% elsif current_user&.admin_level == "viewer" %>
|
|
||||||
<% viewer_tool(nil, "div") do %>
|
|
||||||
<%= link_to admin_trust_level_audit_logs_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(admin_trust_level_audit_logs_path) || request.path.start_with?('/admin/trust_level_audit_logs') ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Trust Level Logs
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if current_user&.admin_level == "admin" || current_user&.admin_level == "superadmin" %>
|
|
||||||
<% admin_tool(nil, "div") do %>
|
|
||||||
<%= link_to admin_admin_api_keys_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(admin_admin_api_keys_path) || request.path.start_with?('/admin/admin_api_keys') ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Admin API Keys
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% elsif current_user&.admin_level == "viewer" %>
|
|
||||||
<% viewer_tool(nil, "div") do %>
|
|
||||||
<%= link_to admin_admin_api_keys_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(admin_admin_api_keys_path) || request.path.start_with?('/admin/admin_api_keys') ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Admin API Keys
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% superadmin_tool(nil, "div") do %>
|
|
||||||
<%= link_to admin_admin_users_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(admin_admin_users_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Admin Management
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% superadmin_tool(nil, "div") do %>
|
|
||||||
<%= link_to admin_deletion_requests_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(admin_deletion_requests_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Account Deletions
|
|
||||||
<% pending_count = DeletionRequest.pending.count %>
|
|
||||||
<% if pending_count > 0 %>
|
|
||||||
<span class="ml-1 px-2 py-0.5 text-xs rounded-full bg-primary text-white font-semibold"><%= pending_count %></span>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% superadmin_tool(nil, "div") do %>
|
|
||||||
<%= link_to good_job_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(good_job_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
GoodBoy
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% superadmin_tool(nil, "div") do %>
|
|
||||||
<%= link_to admin_oauth_applications_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(admin_oauth_applications_path) || request.path.start_with?('/admin/oauth_applications') ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
All OAuth Apps
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% superadmin_tool(nil, "div") do %>
|
|
||||||
<%= link_to flipper_path, class: "block px-2 py-1 rounded-lg transition #{current_page?(flipper_path) ? 'bg-primary text-primary' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
|
||||||
Feature Flags
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= render_activities(@activities) if defined?(@activities) %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div>
|
||||||
|
<%= link_to leaderboards_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(leaderboards_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Leaderboards
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% unless current_user %>
|
||||||
|
<div>
|
||||||
|
<%= link_to docs_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(docs_path) || request.path.start_with?('/docs') ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Docs
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= link_to extensions_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(extensions_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Extensions
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= link_to "/what-is-hackatime", class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?('/what-is-hackatime') ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
What is Hackatime?
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if current_user %>
|
||||||
|
<div>
|
||||||
|
<%= link_to my_projects_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(my_projects_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Projects
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= link_to docs_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(docs_path) || request.path.start_with?('/docs') ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Docs
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= link_to extensions_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(extensions_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Extensions
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= link_to my_settings_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(my_settings_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Settings
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= link_to oauth_applications_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(oauth_applications_path) || request.path.start_with?('/oauth/applications') ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
My OAuth Apps
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a type="button" onclick="showLogout()" class="w-full text-left block px-3 py-2 rounded-md text-sm transition-colors hover:bg-darkless">Logout</a>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if current_user&.admin_level.present? || Rails.env.development? %>
|
||||||
|
<div class="pt-2 mt-2 border-t border-darkless space-y-1">
|
||||||
|
<% dev_tool(nil, "div") do %>
|
||||||
|
<%= link_to letter_opener_web_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(letter_opener_web_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Letter Opener
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% dev_tool(nil, "div") do %>
|
||||||
|
<%= link_to '/rails/mailers', class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?('/rails/mailers') ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Mailers
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if current_user&.admin_level == "admin" || current_user&.admin_level == "superadmin" %>
|
||||||
|
<% admin_tool(nil, "div") do %>
|
||||||
|
<%= link_to admin_timeline_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(admin_timeline_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Review Timeline
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% elsif current_user&.admin_level == "viewer" %>
|
||||||
|
<% viewer_tool(nil, "div") do %>
|
||||||
|
<%= link_to admin_timeline_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(admin_timeline_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Review Timeline
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if current_user&.admin_level == "admin" || current_user&.admin_level == "superadmin" %>
|
||||||
|
<% admin_tool(nil, "div") do %>
|
||||||
|
<%= link_to admin_trust_level_audit_logs_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(admin_trust_level_audit_logs_path) || request.path.start_with?('/admin/trust_level_audit_logs') ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Trust Level Logs
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% elsif current_user&.admin_level == "viewer" %>
|
||||||
|
<% viewer_tool(nil, "div") do %>
|
||||||
|
<%= link_to admin_trust_level_audit_logs_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(admin_trust_level_audit_logs_path) || request.path.start_with?('/admin/trust_level_audit_logs') ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Trust Level Logs
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if current_user&.admin_level == "admin" || current_user&.admin_level == "superadmin" %>
|
||||||
|
<% admin_tool(nil, "div") do %>
|
||||||
|
<%= link_to admin_admin_api_keys_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(admin_admin_api_keys_path) || request.path.start_with?('/admin/admin_api_keys') ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Admin API Keys
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% elsif current_user&.admin_level == "viewer" %>
|
||||||
|
<% viewer_tool(nil, "div") do %>
|
||||||
|
<%= link_to admin_admin_api_keys_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(admin_admin_api_keys_path) || request.path.start_with?('/admin/admin_api_keys') ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Admin API Keys
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% superadmin_tool(nil, "div") do %>
|
||||||
|
<%= link_to admin_admin_users_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(admin_admin_users_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Admin Management
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% superadmin_tool(nil, "div") do %>
|
||||||
|
<%= link_to admin_deletion_requests_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(admin_deletion_requests_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Account Deletions
|
||||||
|
<% pending_count = DeletionRequest.pending.count %>
|
||||||
|
<% if pending_count > 0 %>
|
||||||
|
<span class="ml-1 px-1.5 py-0.5 text-xs rounded-full bg-primary text-white font-medium"><%= pending_count %></span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% superadmin_tool(nil, "div") do %>
|
||||||
|
<%= link_to good_job_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(good_job_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
GoodBoy
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% superadmin_tool(nil, "div") do %>
|
||||||
|
<%= link_to admin_oauth_applications_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(admin_oauth_applications_path) || request.path.start_with?('/admin/oauth_applications') ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
All OAuth Apps
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% superadmin_tool(nil, "div") do %>
|
||||||
|
<%= link_to flipper_path, class: "block px-3 py-2 rounded-md text-sm transition-colors #{current_page?(flipper_path) ? 'bg-primary text-white' : 'hover:bg-darkless'}", data: { action: "click->nav#clickLink" } do %>
|
||||||
|
Feature Flags
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if defined?(@activities) %>
|
||||||
|
<div class="pt-2">
|
||||||
|
<%= render_activities(@activities) %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
<% if is_broken %>
|
<% if is_broken %>
|
||||||
<div class="gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
<div class="gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||||
<p class="text-sm text-yellow-200/80 leading-relaxed">
|
<p class="text-sm text-yellow-200/80 leading-relaxed">
|
||||||
Your editor may not be set up properly—we're receiving invalid project names. This time is shown here but won't be counted accurately and may not be submitted to Hack Club programs.
|
Your editor may not be set up properly - we're receiving invalid project names. This time is shown here but won't be counted accurately and may not be submitted to Hack Club programs.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -1,235 +0,0 @@
|
||||||
<div class="container">
|
|
||||||
<% if current_user&.trust_level == "red" %>
|
|
||||||
<div class="text-primary bg-red-500/10 border-2 border-red-500/20 p-4 text-center rounded-lg mb-4">
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M8 14.5a6.5 6.5 0 1 0 0-13a6.5 6.5 0 0 0 0 13M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m1-5a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-.25-6.25a.75.75 0 0 0-1.5 0v3.5a.75.75 0 0 0 1.5 0z" clip-rule="evenodd" /></svg>
|
|
||||||
<span class="text-3xl font-bold block ml-2">Hold up! Your account has been banned for suspicious activity.</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-primary text-left text-lg mb-2"><b>What does this mean?</b> Your account was convicted for fraud or abuse of Hackatime, such as using methods to gain an unfair advantage on the leaderboards or attempting to manipulate your coding time in any way. This restricts your access to participate in public leaderboards, but Hackatime will still track and display your time. This may also affect your ability to participate in current and future Hack Club events.</p>
|
|
||||||
<p class="text-primary text-left text-lg mb-2"><b>What can I do?</b> Account bans are non-negotiable, and will not be removed unless determined to have been issued incorrectly. In that case, it will automatically be removed. We take fraud very seriously and have a zero-tolerance policy for abuse. If you believe this was a mistake, please DM the <a href="https://hackclub.slack.com/team/U091HC53CE8" target="_blank" class="underline">Fraud Department</a> on Slack. We do not respond in any other channel, DM or thread.</p>
|
|
||||||
<p class="text-primary text-left text-lg mb-0"><b>Can I know what caused this?</b> No. We do not disclose the patterns that were detected. Releasing this information would only benefit fraudsters. The fraud team regularly investigates claims of false bans to increase the effectiveness of our detection systems to combat fraud.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="flex items-center space-x-2 mt-2">
|
|
||||||
<p class="italic text-gray-400 m-0">
|
|
||||||
<%= @flavor_text %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div id="clock" class="clockicons clock-display"></div>
|
|
||||||
<% if current_user %>
|
|
||||||
<h1 class="font-bold mt-1 mb-4 text-5xl text-center">Keep Track of <span class="text-primary">Your</span> Coding Time</h1>
|
|
||||||
<% else %>
|
|
||||||
<h1 class="font-bold mt-1 mb-4 text-5xl text-center">Track How Much You <span class="text-primary">Code</span></h1>
|
|
||||||
<div class="flex flex-col w-full max-w-[50vw] mx-auto mb-22">
|
|
||||||
<%= link_to hca_auth_path(continue: @continue_param), class: "inline-flex items-center justify-center w-full px-6 py-3 rounded text-white font-bold bg-primary hover:bg-primary/75 transition-colors", data: { turbo: false }, onclick: "let s=this.querySelector('.spinner'),i=this.querySelector('.icon');s.classList.remove('hidden');i.classList.add('hidden');this.style.cssText='pointer-events:none;opacity:0.7'" do %>
|
|
||||||
<span class="spinner mr-2 hidden"><%= render "shared/spinner", class: "h-6 w-6" %></span>
|
|
||||||
<img src="/images/icon-rounded.png" class="icon h-6 w-6 mr-2">
|
|
||||||
<span>Sign in with your Hack Club account</span>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="flex items-center my-4">
|
|
||||||
<div class="flex-1 border-t border-darkless"></div>
|
|
||||||
<span class="px-4 text-gray-400 text-sm">or</span>
|
|
||||||
<div class="flex-1 border-t border-darkless"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<%= form_tag email_auth_path, class: "relative flex-1", data: { turbo: false } do %>
|
|
||||||
<div class="relative">
|
|
||||||
<%= email_field_tag :email, nil, placeholder: "Enter your email to get a sign in link", required: true, class: "w-full px-3 py-3 pr-12 border border-darkless bg-dark placeholder-secondary rounded focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" %>
|
|
||||||
<button type="submit" class="absolute right-2 top-1/2 transform -translate-y-1/2 w-8 h-8 p-1 bg-blue-600 hover:bg-blue-700 rounded cursor-pointer border-none flex items-center justify-center transition-colors">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M13.3 20.275q-.3-.3-.3-.7t.3-.7L16.175 16H7q-.825 0-1.412-.587T5 14V5q0-.425.288-.712T6 4t.713.288T7 5v9h9.175l-2.9-2.9q-.3-.3-.288-.7t.288-.7q.3-.3.7-.312t.7.287L19.3 14.3q.15.15.212.325t.063.375t-.063.375t-.212.325l-4.575 4.575q-.3.3-.712.3t-.713-.3" /></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= link_to slack_auth_path, class: "flex items-center justify-center px-4 py-3 rounded text-white cursor-pointer bg-dark hover:bg-darkless border border-darkless text-gray-300 transition-colors w-1/4 gap-2" do %>
|
|
||||||
<span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-6 h-6"><path fill="currentColor" d="M6 15a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2h2zm1 0a2 2 0 0 1 2-2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2a2 2 0 0 1-2-2zm2-8a2 2 0 0 1-2-2a2 2 0 0 1 2-2a2 2 0 0 1 2 2v2zm0 1a2 2 0 0 1 2 2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2a2 2 0 0 1 2-2zm8 2a2 2 0 0 1 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2h-2zm-1 0a2 2 0 0 1-2 2a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2a2 2 0 0 1 2 2zm-2 8a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2v-2zm0-1a2 2 0 0 1-2-2a2 2 0 0 1 2-2h5a2 2 0 0 1 2 2a2 2 0 0 1-2 2z" /></svg></span>
|
|
||||||
<span class="hidden xl:inline">Slack Sign In</span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% if params[:sign_in_email] %>
|
|
||||||
<div class="text-green-500 mt-4 text-center max-w-[50vw] mx-auto">Check your email for a sign-in link!</div>
|
|
||||||
<% dev_tool class: "text-center max-w-[50vw] mx-auto mb-4" do %>
|
|
||||||
Because you're on localhost, <%= link_to "click here to view the email", letter_opener_web_path %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<div class="w-full flex justify-center overflow-x-none">
|
|
||||||
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">==============================================/ h a c k /=============================================</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-8 mb-8">
|
|
||||||
<h1 class="font-bold mt-1 mb-1 text-4xl">Compatible with your favourite IDEs</h1>
|
|
||||||
<p class="text-primary monospace text-[20px]">Hackatime works with these code editors and more!</p>
|
|
||||||
<div id="supported-editors" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4 mt-4">
|
|
||||||
<% popular_editors = [['VS Code', 'vs-code'], %w[PyCharm pycharm], ['IntelliJ IDEA', 'intellij-idea'], ['Sublime Text', 'sublime-text'], %w[Vim vim], %w[Neovim neovim], ['Android Studio', 'android-studio'], %w[Xcode xcode], %w[Unity unity], %w[Godot godot], %w[Cursor cursor], %w[Zed zed], %w[Terminal terminal], %w[WebStorm webstorm], %w[Eclipse eclipse], %w[Emacs emacs], %w[Jupyter jupyter], %w[OnShape onshape]] %>
|
|
||||||
<% popular_editors.each do |name, slug| %>
|
|
||||||
<a href="<%= doc_path("editors/#{slug}") %>" class="bg-darkless rounded-lg p-3 hover:bg-primary/20 transition-all duration-200 text-center block hover:-translate-y-0.5 hover:shadow-lg hover:shadow-primary/20">
|
|
||||||
<img src="/images/editor-icons/<%= slug %>-128.png" alt="<%= name %>" class="w-12 h-12 mx-auto mb-2">
|
|
||||||
<div class="text-sm text-white"><%= name %></div>
|
|
||||||
</a>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-full flex justify-center overflow-x-none">
|
|
||||||
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">-----------------------------------------------------------------------------------------------</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-8 mb-8">
|
|
||||||
<h1 class="font-bold mt-1 mb-1 text-4xl">Why Hackatime?</h1>
|
|
||||||
<% if @home_stats&.[](:seconds_tracked) && @home_stats&.[](:users_tracked) %>
|
|
||||||
<p class="text-primary monospace text-[20px]">
|
|
||||||
We've tracked over <span class="text-primary"><%= number_with_delimiter(@home_stats[:seconds_tracked] / 3600) %> <%= 'hour'.pluralize(@home_stats[:seconds_tracked] / 3600) %> </span> of coding time across <span class="text-primary"><%= number_with_delimiter(@home_stats[:users_tracked]) %> <%= 'high schooler'.pluralize(@home_stats[:users_tracked]) %> </span> since <span class="text-primary">2025</span>!
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
<div class="overflow-x-auto -mx-4 px-4 pb-4 no-scrollbar">
|
|
||||||
<div class="grid grid-cols-4 gap-4 mt-4 text-center h-30 min-w-[800px]">
|
|
||||||
<p class="flex flex-col text-3xl justify-center bg-darkless rounded-lg p-3"><span class="text-primary font-bold text-4xl">100%</span><br>free</p>
|
|
||||||
<p class="flex flex-col text-3xl justify-center bg-darkless rounded-lg p-3">works<br><span class="text-primary font-bold text-4xl">offline</span></p>
|
|
||||||
<p class="flex flex-col text-3xl justify-center bg-darkless rounded-lg p-3"><span class="text-primary font-bold text-4xl">real time</span><br>stats</p>
|
|
||||||
<p class="flex flex-col text-3xl justify-center bg-darkless rounded-lg p-3">rise to the<br><span class="text-primary font-bold text-4xl">top #1</span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-full flex justify-center overflow-x-none">
|
|
||||||
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">--------------------------------| #hackclub | #hackclub | #hackclub |--------------------------------</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-8 mb-8">
|
|
||||||
<h1 class="font-bold mt-1 mb-1 text-4xl">For your favorite <span class="text-primary">Hack Club</span> events!</h1>
|
|
||||||
<p class="text-primary monospace text-[20px]">First class support for Hack Club events, and more.</p>
|
|
||||||
<div class="relative mt-4 mb-4 rounded-lg" style="background: linear-gradient(90deg, #2e1538 0%, #5a2d4a 50%, #a36b80 100%);">
|
|
||||||
<div class="flex flex-col md:flex-row">
|
|
||||||
<div class="w-full md:w-1/2 pl-8">
|
|
||||||
<img src="/images/flagship.png" class="h-auto w-full mx-auto md:mx-0">
|
|
||||||
</div>
|
|
||||||
<div class="w-full md:w-1/2 p-8 pl-4 pr-4 grid grid-cols-1 gap-4 ">
|
|
||||||
<div class="p-8 rounded-lg bg-no-repeat bg-center relative" style="background-image: url('/images/bgBanner.png'); background-size: 100% 100%;">
|
|
||||||
<h2 class="font-bold text-[22px] text-white drop-shadow-md mb-4 mt-6">Make your first game, get a free ticket to the craziest game jam of 2026!</h2>
|
|
||||||
<p class="text-white drop-shadow-md text-[18px] mb-4">Meet indie game developers and your favorite YouTubers.</p>
|
|
||||||
<%= link_to "Start building", "https://flagship.hackclub.com/?utm_source=hackatime", class: "inline-block relative z-10 font-primary font-bold px-6 py-3 rounded-full shadow-lg hover:scale-105 hover:shadow-xl transition-all duration-200", style: "background-color: #f5f0e8; color: #2a2a2a; font-size: 18px; padding-top: 14px;", target: "_blank" %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col md:flex-row bg-linear-to-r from-[#EFCCCC] to-[#D35648] mt-4 mb-4 rounded-lg">
|
|
||||||
<div class="w-full md:w-1/3 -translate-y-5">
|
|
||||||
<img src="/images/athena.png" class="w-[400px]">
|
|
||||||
</div>
|
|
||||||
<div class="w-full md:w-2/3 p-8 pl-4 pr-4">
|
|
||||||
<img src="/images/athena_award.svg" class="h-24 mb-4">
|
|
||||||
<p class="text-[18px] m-4">Earn an <b>industry recognized technical certificate</b> for coding 30 hours and building 3 personal projects. Win prizes as you code, and a chance to travel to NYC for 2025's largest high school hackathon for girls.</p>
|
|
||||||
<%= link_to "Join Athena", "https://athena.hackclub.com/", class: "inline-block bg-white font-primary font-bold text-[#D35648] px-4 py-2 m-4 rounded-[100px] text-[22px] hover:scale-105 transition-transform duration-200", target: "_blank" %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if current_user %>
|
|
||||||
<% if @show_wakatime_setup_notice %>
|
|
||||||
<div class="text-left my-8 flex flex-col">
|
|
||||||
<p class="mb-4 text-xl text-primary">Hello friend! Looks like you are new around here, let's get you set up so you can start tracking your coding time.</p>
|
|
||||||
<%= link_to "Let's setup Hackatime! Click me :D", my_wakatime_setup_path, class: "inline-block w-auto text-3xl font-bold px-8 py-4 bg-primary text-white rounded shadow-md hover:shadow-lg hover:-translate-y-1 transition-all duration-300 animate-pulse" %>
|
|
||||||
<div class="flex items-center mt-4 flex-nowrap">
|
|
||||||
<% if @ssp_users_recent&.any? %>
|
|
||||||
<div class="flex m-0 ml-0 shrink-0">
|
|
||||||
<% @ssp_users_recent.each_with_index do |user, index| %>
|
|
||||||
<div class="relative cursor-pointer transition-transform duration-200 hover:-translate-y-1 hover:z-10 group <%= index > 0 ? '-ml-4' : '' %>">
|
|
||||||
<div class="absolute -top-9 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-2 py-1 rounded text-xs whitespace-nowrap opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-20">
|
|
||||||
<%= h(user[:display_name]) %>
|
|
||||||
<div class="absolute top-full left-1/2 -ml-1 border-l-2 border-r-2 border-t-2 border-transparent border-t-gray-800"></div>
|
|
||||||
</div>
|
|
||||||
<img src="<%= user[:avatar_url] %>" alt="<%= h(user[:display_name]) %>" class="w-10 h-10 rounded-full border-2 border-primary object-cover shadow-sm">
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if @ssp_users_size && @ssp_users_size > 5 %>
|
|
||||||
<div class="relative cursor-pointer transition-transform duration-200 hover:-translate-y-1 hover:z-10 group -ml-4" title="See all <%= @ssp_users_size %> users">
|
|
||||||
<div class="w-10 h-10 rounded-full border-2 border-primary bg-primary text-white font-bold text-sm flex items-center justify-center shadow-sm">+<%= @ssp_users_size - 5 %></div>
|
|
||||||
<div class="absolute -left-5 top-11 bg-gray-800 rounded-lg shadow-xl p-4 w-80 z-50 max-h-96 overflow-y-auto opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
|
||||||
<h4 class="mt-0 mb-2 text-base text-gray-200 border-b border-gray-600 pb-2">All users who set up Hackatime</h4>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<% @ssp_users_recent.each do |user| %>
|
|
||||||
<div class="flex items-center p-1 rounded hover:bg-gray-700 transition-colors duration-200">
|
|
||||||
<img src="<%= user[:avatar_url] %>" alt="<%= h(user[:display_name]) %>" class="w-8 h-8 rounded-full mr-2 border border-primary">
|
|
||||||
<span class="font-medium text-sm"><%= h(user[:display_name]) %></span>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div class="absolute -top-2 left-8 w-0 h-0 border-l-2 border-r-2 border-b-2 border-transparent border-b-gray-800"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if @ssp_message %>
|
|
||||||
<p class="m-0 ml-2 italic text-gray-400"><%= @ssp_message %> (this is real data)</p>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if current_user.github_uid.blank? %>
|
|
||||||
<div class="bg-dark border border-primary rounded-lg p-4 mb-6">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="text-white shrink-0"><path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" /></svg>
|
|
||||||
<div class="flex-1">
|
|
||||||
<span class="text-white">Link your GitHub account to unlock project linking, show what you're working on, and qualify for leaderboards!</span>
|
|
||||||
</div>
|
|
||||||
<%= link_to "Connect GitHub", "/auth/github", class: "bg-primary hover:bg-primary text-white font-medium px-4 py-2 rounded-lg transition-colors duration-200 flex-shrink-0", data: { turbo: false } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<p class="mt-2">
|
|
||||||
<% if @show_logged_time_sentence %>
|
|
||||||
You've logged
|
|
||||||
<%= short_time_detailed @todays_duration %>
|
|
||||||
<% if @todays_languages.any? || @todays_editors.any? %>
|
|
||||||
across
|
|
||||||
<% if @todays_languages.any? %>
|
|
||||||
<% if @todays_languages.length >= 4 %>
|
|
||||||
<%= @todays_languages[0..1].join(", ") %> <span title="<%= @todays_languages[2..].join(", ") %>">(& <%= pluralize(@todays_languages.length - 2, 'other language') %>)</span>
|
|
||||||
<% else %>
|
|
||||||
<%= @todays_languages.to_sentence %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% if @todays_languages.any? && @todays_editors.any? %>
|
|
||||||
using
|
|
||||||
<% end %>
|
|
||||||
<% if @todays_editors.any? %>
|
|
||||||
<%= @todays_editors.to_sentence %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% else %>
|
|
||||||
No time logged today... but you can change that!
|
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
<%= turbo_frame_tag "mini_leaderboard", src: mini_leaderboard_static_pages_path, loading: :lazy do %>
|
|
||||||
<%= render "leaderboards/mini_leaderboard_loading" %>
|
|
||||||
<% end %>
|
|
||||||
<%= turbo_frame_tag "filterable_dashboard", src: filterable_dashboard_static_pages_path, loading: :lazy do %>
|
|
||||||
<%= render "static_pages/filterable_dashboard_loading" %>
|
|
||||||
<% end %>
|
|
||||||
<%= turbo_frame_tag "activity_graph", src: activity_graph_static_pages_path, loading: :lazy do %>
|
|
||||||
<%= render 'static_pages/activity_graph_loading' %>
|
|
||||||
<% end %>
|
|
||||||
<% else %>
|
|
||||||
<% if @leaderboard %>
|
|
||||||
<h3>Today's Top Hack Clubbers</h3>
|
|
||||||
<%= render "leaderboards/mini_leaderboard", leaderboard: @leaderboard, current_user: nil %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="w-full flex justify-center overflow-x-none">
|
|
||||||
<p class="monospace text-center text-primary text-[22px] select-none whitespace-nowrap">==============================================/ h a c k /=============================================</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 my-8 items-center">
|
|
||||||
<div>
|
|
||||||
<h1 class="font-bold mt-1 mb-1 text-5xl">Start hacking with <span class="text-primary">Hackatime</span> now!</h1>
|
|
||||||
<p class="text-primary monospace text-[20px]">It is super easy to setup, here is a quick guide!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full relative pb-[56.25%] h-0 overflow-hidden">
|
|
||||||
<iframe width="1280" height="720" src="https://www.youtube-nocookie.com/embed/FSIxV4u77WQ?rel=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen class="absolute top-0 left-0 w-full h-full rounded-lg"></iframe>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="max-w-4xl mx-auto">
|
<div class="max-w-4xl mx-auto">
|
||||||
<h1 class="text-4xl font-bold mb-6 text-center">What is <span class="text-primary">Hackatime</span>?</h1>
|
<h1 class="text-4xl font-bold mb-6 mt-4 text-center">What is <span class="text-primary">Hackatime</span>?</h1>
|
||||||
|
|
||||||
<div class="bg-dark rounded-lg p-8 mb-8">
|
<div class="bg-dark rounded-lg p-8 mb-8">
|
||||||
<p class="text-lg mb-6"><strong class="text-primary">Hackatime</strong> is a free, open-source coding time tracker built by <a href="https://hackclub.com" target="_blank" class="text-primary hover:text-red underline">Hack Club</a> for high school students and developers who want to understand their programming habits.</p>
|
<p class="text-lg mb-6"><strong class="text-primary">Hackatime</strong> is a free, open-source coding time tracker built by <a href="https://hackclub.com" target="_blank" class="text-primary hover:text-red underline">Hack Club</a> for high school students and developers who want to understand their programming habits.</p>
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center text-gray-400 text-sm">
|
<div class="text-center text-gray-400 text-sm pb-4">
|
||||||
<p><strong>Hackatime</strong> is built and maintained by the Hack Club community. <%= link_to 'Learn more about Hack Club', 'https://hackclub.com', target: '_blank', class: 'text-primary hover:text-red underline' %>.</p>
|
<p><strong>Hackatime</strong> is built and maintained by the Hack Club community. <%= link_to 'Learn more about Hack Club', 'https://hackclub.com', target: '_blank', class: 'text-primary hover:text-red underline' %>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,6 @@
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 md:col-span-2">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 md:col-span-2">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">🚀</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white">Time Tracking Wizard</h2>
|
<h2 class="text-xl font-semibold text-white">Time Tracking Wizard</h2>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-300 mb-4">Get started with tracking your coding time in just a few minutes.</p>
|
<p class="text-gray-300 mb-4">Get started with tracking your coding time in just a few minutes.</p>
|
||||||
|
|
@ -24,9 +21,6 @@
|
||||||
|
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">🌍</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_region">Region</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_region">Region</h2>
|
||||||
</div>
|
</div>
|
||||||
<%= form_with model: @user,
|
<%= form_with model: @user,
|
||||||
|
|
@ -49,9 +43,6 @@
|
||||||
|
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">⚙️</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_hackatime_extension">Extension Settings</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_hackatime_extension">Extension Settings</h2>
|
||||||
</div>
|
</div>
|
||||||
<%= form_with model: @user,
|
<%= form_with model: @user,
|
||||||
|
|
@ -68,9 +59,6 @@
|
||||||
|
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">🪪</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_username">Username</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_username">Username</h2>
|
||||||
</div>
|
</div>
|
||||||
<%= form_with model: @user,
|
<%= form_with model: @user,
|
||||||
|
|
@ -94,9 +82,6 @@
|
||||||
|
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">💬</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_slack_status">Slack Integration</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_slack_status">Slack Integration</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -139,9 +124,6 @@
|
||||||
|
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">🔒</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_privacy">Privacy Settings</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_privacy">Privacy Settings</h2>
|
||||||
</div>
|
</div>
|
||||||
<%= form_with model: @user,
|
<%= form_with model: @user,
|
||||||
|
|
@ -158,9 +140,6 @@
|
||||||
|
|
||||||
<div class="border-t border-darkless pt-4 mt-4 space-y-3">
|
<div class="border-t border-darkless pt-4 mt-4 space-y-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">🗑️</span>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-semibold text-white" id="delete_account">Delete Account</h3>
|
<h3 class="text-lg font-semibold text-white" id="delete_account">Delete Account</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -175,9 +154,6 @@
|
||||||
|
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">🔑</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_api_key">API Key</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_api_key">API Key</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -190,9 +166,6 @@
|
||||||
|
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 md:col-span-2">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 md:col-span-2">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">🔗</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_github_account">Connected Accounts</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_github_account">Connected Accounts</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -251,9 +224,6 @@
|
||||||
|
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">📊</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_stats_badges">Stats Badges</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_stats_badges">Stats Badges</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -321,9 +291,6 @@
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 space-y-6">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">📄</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_config_file">Config File</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_config_file">Config File</h2>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-300 text-sm mb-4">Your Wakatime configuration file for tracking coding time.</p>
|
<p class="text-gray-300 text-sm mb-4">Your Wakatime configuration file for tracking coding time.</p>
|
||||||
|
|
@ -336,9 +303,6 @@
|
||||||
|
|
||||||
<div class="border-t border-darkless pt-6">
|
<div class="border-t border-darkless pt-6">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">🚚</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_migration_assistant">Migration Assistant</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_migration_assistant">Migration Assistant</h2>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-300 text-sm mb-4">This will migrate your heartbeats from waka.hackclub.com to this platform.</p>
|
<p class="text-gray-300 text-sm mb-4">This will migrate your heartbeats from waka.hackclub.com to this platform.</p>
|
||||||
|
|
@ -358,9 +322,6 @@
|
||||||
|
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 md:col-span-2">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 md:col-span-2">
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">📝</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="user_markscribe">Markscribe Templates</h2>
|
<h2 class="text-xl font-semibold text-white" id="user_markscribe">Markscribe Templates</h2>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-300 text-sm mb-4">Use markscribe to create beautiful GitHub profile READMEs with your coding stats.</p>
|
<p class="text-gray-300 text-sm mb-4">Use markscribe to create beautiful GitHub profile READMEs with your coding stats.</p>
|
||||||
|
|
@ -384,9 +345,6 @@
|
||||||
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 md:col-span-2">
|
<div class="border border-primary rounded-xl p-6 bg-dark transition-all duration-200 md:col-span-2">
|
||||||
<% if @user.trust_level == "red" %>
|
<% if @user.trust_level == "red" %>
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">💾</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="download_user_data">Download Your Data</h2>
|
<h2 class="text-xl font-semibold text-white" id="download_user_data">Download Your Data</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-red-500/20 border border-red-500 rounded-lg p-4">
|
<div class="bg-red-500/20 border border-red-500 rounded-lg p-4">
|
||||||
|
|
@ -397,9 +355,6 @@
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex items-center gap-3 mb-4">
|
<div class="flex items-center gap-3 mb-4">
|
||||||
<div class="p-2 bg-primary/10 rounded">
|
|
||||||
<span class="text-2xl">💾</span>
|
|
||||||
</div>
|
|
||||||
<h2 class="text-xl font-semibold text-white" id="download_user_data">Download Your Data</h2>
|
<h2 class="text-xl font-semibold text-white" id="download_user_data">Download Your Data</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,355 +0,0 @@
|
||||||
<div class="min-h-screen text-white">
|
|
||||||
<div class="max-w-6xl mx-auto p-4">
|
|
||||||
<div class="text-center mb-4">
|
|
||||||
<h1 class="text-4xl font-bold text-primary mb-2">Hackatime Setup</h1>
|
|
||||||
<div class="flex items-center justify-center gap-2 mb-4">
|
|
||||||
<div class="w-8 h-8 bg-primary rounded-full flex items-center justify-center text-sm font-bold">1</div>
|
|
||||||
<div class="w-16 h-1 bg-darkless"></div>
|
|
||||||
<div class="w-8 h-8 bg-darkless rounded-full flex items-center justify-center text-sm">2</div>
|
|
||||||
<div class="w-16 h-1 bg-darkless"></div>
|
|
||||||
<div class="w-8 h-8 bg-darkless rounded-full flex items-center justify-center text-sm">3</div>
|
|
||||||
<div class="w-16 h-1 bg-darkless"></div>
|
|
||||||
<div class="w-8 h-8 bg-darkless rounded-full flex items-center justify-center text-sm">4</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
<div class="lg:col-span-2">
|
|
||||||
<div class="setup-instructions">
|
|
||||||
<div id="mac-linux" class="bg-dark rounded-lg p-6" style="display: none;">
|
|
||||||
<h3 class="text-2xl font-bold text-green mb-4">🍎 Mac/Linux/Codespaces</h3>
|
|
||||||
|
|
||||||
<details class="mb-4 group">
|
|
||||||
<summary class="cursor-pointer text-blue hover:text-cyan flex items-center gap-2 border border-blue/30 rounded-lg p-3 bg-blue/10">
|
|
||||||
<svg class="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
<span class="font-semibold">On GitHub Codespaces? Click here first!</span>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-3 pl-6 border-l-2 border-blue/30">
|
|
||||||
<p class="text-md mb-2">Here's how to find your terminal:</p>
|
|
||||||
<ol class="list-decimal list-inside text-md space-y-1">
|
|
||||||
<li>Look at the bottom of your Codespaces window for a panel</li>
|
|
||||||
<li>Click the <strong>"Terminal"</strong> tab (it's usually already open!)</li>
|
|
||||||
<li>If you don't see it, press <kbd class="bg-darkless px-1 rounded text-sm">Ctrl+`</kbd> (or <kbd class="bg-darkless px-1 rounded text-sm">Cmd+`</kbd> on Mac)</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div class="space-y-4 mb-4">
|
|
||||||
<ol class="list-decimal list-inside text-lg space-y-3">
|
|
||||||
<li>
|
|
||||||
<strong>Find your terminal:</strong>
|
|
||||||
<ul class="list-disc list-inside ml-4 mt-1 text-md space-y-1">
|
|
||||||
<li><strong>Mac:</strong> Open the "Terminal" app (search in Spotlight)</li>
|
|
||||||
<li><strong>Linux:</strong> Open your terminal emulator</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><strong>Copy the command below</strong> (click the Copy button!)</li>
|
|
||||||
<li><strong>Paste it in your terminal</strong> and press <i>Enter</i></li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="code-block bg-darkless rounded-lg p-4 mb-4">
|
|
||||||
<code class="text-cyan text-sm">curl -fsSL https://hack.club/setup/install.sh | bash -s -- <%= @current_user_api_key %></code>
|
|
||||||
<button class="copy-button bg-primary hover:bg-primary/75 border-0 text-white px-6 py-2 rounded transition-colors cursor-pointer font-semibold" onclick="copy(this)">Copy</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<details class="mb-4 group">
|
|
||||||
<summary class="cursor-pointer text-secondary hover:text-white flex items-center gap-2">
|
|
||||||
<svg class="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
Watch a video tutorial
|
|
||||||
</summary>
|
|
||||||
<div class="mt-3 rounded-lg">
|
|
||||||
<iframe width="100%" height="250" src="https://www.youtube.com/embed/QTwhJy7nT_w?loop=1&playlist=QTwhJy7nT_w&modestbranding=1&rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen class="rounded"></iframe>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div class="flex gap-3 flex-wrap">
|
|
||||||
<button class="bg-blue hover:bg-blue/75 text-white border-0 px-4 py-2 rounded transition-colors cursor-pointer text-sm" onclick="toggleSection('windows')">Using Windows?</button>
|
|
||||||
<button class="bg-purple hover:bg-purple/75 text-white border-0 px-4 py-2 rounded transition-colors cursor-pointer text-sm" onclick="toggleSection('advanced')">Custom Setup</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="windows" class="bg-dark rounded-lg p-6" style="display: none;">
|
|
||||||
<h3 class="text-2xl font-bold text-blue mb-4">🪟 Windows</h3>
|
|
||||||
|
|
||||||
<div class="space-y-4 mb-4">
|
|
||||||
<ol class="list-decimal list-inside text-lg space-y-3">
|
|
||||||
<li><strong>Open PowerShell:</strong> Press <kbd class="bg-darkless px-2 py-1 rounded text-md">Windows + R</kbd>, type "powershell", press Enter</li>
|
|
||||||
<li><strong>Copy the command below</strong> (click the Copy button!)</li>
|
|
||||||
<li><strong>Paste it</strong> (right-click in PowerShell, or <kbd class="bg-darkless px-1 rounded text-md">Ctrl+V</kbd>)</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="code-block bg-darkless rounded-lg p-4 mb-4">
|
|
||||||
<code class="text-cyan text-sm">& ([scriptblock]::Create((irm https://hack.club/setup/install.ps1))) -ApiKey <%= @current_user_api_key %></code>
|
|
||||||
<button class="copy-button bg-primary hover:bg-primary/75 border-0 text-white px-6 py-2 rounded transition-colors cursor-pointer font-semibold" onclick="copy(this)">Copy</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<details class="mb-4 group">
|
|
||||||
<summary class="cursor-pointer text-secondary hover:text-white flex items-center gap-2">
|
|
||||||
<svg class="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
Watch a video tutorial
|
|
||||||
</summary>
|
|
||||||
<div class="mt-3 rounded-lg">
|
|
||||||
<iframe width="100%" height="250" src="https://www.youtube.com/embed/fX9tsiRvzhg?loop=1&playlist=fX9tsiRvzhg&modestbranding=1&rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen class="rounded"></iframe>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div class="flex gap-3 flex-wrap">
|
|
||||||
<button class="bg-green hover:bg-green/75 text-white border-0 px-4 py-2 rounded transition-colors cursor-pointer text-sm" onclick="toggleSection('mac-linux')">Using Mac/Linux?</button>
|
|
||||||
<button class="bg-purple hover:bg-purple/75 text-white border-0 px-4 py-2 rounded transition-colors cursor-pointer text-sm" onclick="toggleSection('advanced')">Custom Setup</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="advanced" class="bg-dark rounded-lg p-6" style="display: none;">
|
|
||||||
<h3 class="text-2xl font-bold text-purple mb-4">⚙️ Custom Setup</h3>
|
|
||||||
<div class="bg-purple/10 border border-purple/30 rounded-lg p-4 mb-4">
|
|
||||||
<p class="text-sm">For advanced users who want to manually configure their setup.</p>
|
|
||||||
</div>
|
|
||||||
<p class="text-lg mb-4">Create or edit <span class="bg-darkless mx-2 px-2 py-1 rounded text-cyan text-md">~/.wakatime.cfg</span> with:</p>
|
|
||||||
|
|
||||||
<div class="code-block bg-darkless rounded-lg p-4 mb-4">
|
|
||||||
<code class="text-cyan text-sm">[settings] api_url = <%= api_hackatime_v1_url %> api_key = <%= @current_user_api_key %> heartbeat_rate_limit_seconds = 30</code>
|
|
||||||
<button class="copy-button bg-primary hover:bg-primary/75 border-0 text-white px-6 py-2 rounded transition-colors cursor-pointer font-semibold" onclick="copy(this)">Copy</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-3 flex-wrap">
|
|
||||||
<button class="bg-green hover:bg-green/75 text-white border-0 px-4 py-2 rounded transition-colors cursor-pointer text-sm" onclick="toggleSection('mac-linux')">Using Mac/Linux?</button>
|
|
||||||
<button class="bg-blue hover:bg-blue/75 text-white border-0 px-4 py-2 rounded transition-colors cursor-pointer text-sm" onclick="toggleSection('windows')">Using Windows?</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lg:col-span-1">
|
|
||||||
<div class="lg:sticky lg:top-4">
|
|
||||||
<section id="status-panel" class="bg-dark rounded-lg p-6 border-2 border-darkless">
|
|
||||||
<div id="waiting-state">
|
|
||||||
<div class="text-center mb-4">
|
|
||||||
<div class="w-16 h-16 mx-auto mb-3 rounded-full bg-primary/20 flex items-center justify-center">
|
|
||||||
<svg class="w-8 h-8 text-primary animate-pulse" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 class="text-xl font-bold text-white mb-2">Waiting for you to run the setup command...</h4>
|
|
||||||
<p class="text-secondary text-sm" id="status-message">Copy the command on the left and run it in your terminal!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-darkless rounded-lg p-3">
|
|
||||||
<div class="flex items-center justify-center gap-2 text-secondary">
|
|
||||||
<div class="progress-indicator"></div>
|
|
||||||
<span class="text-sm" id="poll-status">Listening for heartbeats...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="success-state" class="hidden">
|
|
||||||
<div class="text-center mb-4">
|
|
||||||
<div class="w-16 h-16 mx-auto mb-3 rounded-full bg-green/20 flex items-center justify-center">
|
|
||||||
<svg class="w-8 h-8 text-green" 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>
|
|
||||||
</div>
|
|
||||||
<h4 class="text-xl font-bold text-green mb-2">Setup complete <span id="heartbeat-time-ago"></span>!</h4>
|
|
||||||
<p class="text-secondary text-sm">Hackatime is configured and ready to go.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= link_to my_wakatime_setup_step_2_path, class: "block w-full bg-primary hover:bg-primary/75 text-white text-center px-6 py-3 rounded-lg font-semibold transition-colors" do %>
|
|
||||||
Continue →
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<p class="text-center text-secondary text-xs mt-3">Already configured? <a href="<%= my_wakatime_setup_step_2_path %>" class="text-cyan hover:underline">Skip to next step</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function a() {
|
|
||||||
const ua = window.navigator.userAgent;
|
|
||||||
const mac = document.getElementById("mac-linux");
|
|
||||||
const windows = document.getElementById("windows");
|
|
||||||
|
|
||||||
mac.style.display = "none";
|
|
||||||
windows.style.display = "none";
|
|
||||||
|
|
||||||
if (ua.indexOf("Windows") !== -1) {
|
|
||||||
windows.style.display = "block";
|
|
||||||
} else {
|
|
||||||
mac.style.display = "block";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("turbo:load", function () {
|
|
||||||
a();
|
|
||||||
|
|
||||||
window.toggleSection = function (section) {
|
|
||||||
const mac = document.getElementById("mac-linux");
|
|
||||||
const windows = document.getElementById("windows");
|
|
||||||
const advanced = document.getElementById("advanced");
|
|
||||||
|
|
||||||
mac.style.display = "none";
|
|
||||||
windows.style.display = "none";
|
|
||||||
advanced.style.display = "none";
|
|
||||||
|
|
||||||
if (section === "windows") {
|
|
||||||
windows.style.display = "block";
|
|
||||||
} else if (section === "advanced") {
|
|
||||||
advanced.style.display = "block";
|
|
||||||
} else {
|
|
||||||
mac.style.display = "block";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const waitingState = document.getElementById("waiting-state");
|
|
||||||
const successState = document.getElementById("success-state");
|
|
||||||
const statusMessage = document.getElementById("status-message");
|
|
||||||
const pollStatus = document.getElementById("poll-status");
|
|
||||||
const statusPanel = document.getElementById("status-panel");
|
|
||||||
|
|
||||||
let checkCount = 0;
|
|
||||||
const maxChecks = 120;
|
|
||||||
|
|
||||||
const msg = ["Copy the command on the left and run it in your terminal!", "Paste the command and press Enter...", "The script will configure everything automatically!", "Almost there - just run the command!", "We'll detect it as soon as the script runs!"];
|
|
||||||
|
|
||||||
function showSuccess(timeAgo) {
|
|
||||||
waitingState.classList.add("hidden");
|
|
||||||
successState.classList.remove("hidden");
|
|
||||||
statusPanel.classList.remove("border-darkless");
|
|
||||||
statusPanel.classList.add("border-green");
|
|
||||||
document.getElementById("heartbeat-time-ago").textContent = timeAgo;
|
|
||||||
}
|
|
||||||
|
|
||||||
function check() {
|
|
||||||
fetch(<%== api_v1_my_heartbeats_most_recent_path(source_type: "test_entry").to_json %>, {
|
|
||||||
headers: {
|
|
||||||
Authorization: "Bearer " + <%== @current_user_api_key.to_json %>,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.has_heartbeat) {
|
|
||||||
const heartbeatTime = new Date(data.heartbeat.created_at);
|
|
||||||
const now = new Date();
|
|
||||||
const secondsAgo = (now - heartbeatTime) / 1000;
|
|
||||||
const recentThreshold = 300;
|
|
||||||
|
|
||||||
if (secondsAgo <= recentThreshold) {
|
|
||||||
showSuccess(data.time_ago);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error("No heartbeats yet");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
checkCount++;
|
|
||||||
|
|
||||||
if (checkCount % 3 === 0) {
|
|
||||||
const msgIndex = Math.floor(checkCount / 3) % msg.length;
|
|
||||||
statusMessage.textContent = msg[msgIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
pollStatus.textContent = `Checked ${checkCount} time${checkCount === 1 ? "" : "s"}...`;
|
|
||||||
|
|
||||||
if (checkCount >= maxChecks) {
|
|
||||||
pollStatus.textContent = "Still waiting... Make sure you've run the command!";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(check, 5000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
check();
|
|
||||||
|
|
||||||
window.skipToNext = function () {
|
|
||||||
window.location.href = <%== my_wakatime_setup_step_2_path.to_json %>;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function copy(button) {
|
|
||||||
const codeBlock = button.previousElementSibling;
|
|
||||||
const text = codeBlock.textContent;
|
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
|
||||||
const originalText = button.textContent;
|
|
||||||
button.textContent = "✅ Copied!";
|
|
||||||
button.classList.add("bg-green");
|
|
||||||
button.classList.remove("bg-primary");
|
|
||||||
setTimeout(() => {
|
|
||||||
button.textContent = originalText;
|
|
||||||
button.classList.remove("bg-green");
|
|
||||||
button.classList.add("bg-primary");
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.progress-indicator {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid var(--color-darkless);
|
|
||||||
border-top: 2px solid var(--color-primary);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.setup-instructions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block code {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-button {
|
|
||||||
flex-shrink: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.code-block {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block code {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
<div class="min-h-screen text-white">
|
|
||||||
<div class="max-w-6xl mx-auto p-4">
|
|
||||||
<div class="text-center mb-4">
|
|
||||||
<h1 class="text-4xl font-bold text-primary mb-2">Hackatime Setup</h1>
|
|
||||||
<div class="flex items-center justify-center gap-2 mb-4">
|
|
||||||
<div class="w-8 h-8 bg-green rounded-full flex items-center justify-center text-sm font-bold">
|
|
||||||
<svg class="w-5 h-5 text-white" 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>
|
|
||||||
</div>
|
|
||||||
<div class="w-16 h-1 bg-green"></div>
|
|
||||||
<div class="w-8 h-8 bg-primary rounded-full flex items-center justify-center text-sm font-bold">2</div>
|
|
||||||
<div class="w-16 h-1 bg-darkless"></div>
|
|
||||||
<div class="w-8 h-8 bg-darkless rounded-full flex items-center justify-center text-sm">3</div>
|
|
||||||
<div class="w-16 h-1 bg-darkless"></div>
|
|
||||||
<div class="w-8 h-8 bg-darkless rounded-full flex items-center justify-center text-sm">4</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center mb-8">
|
|
||||||
<h2 class="text-3xl font-bold mb-4">What editor do you use?</h2>
|
|
||||||
<p class="text-secondary text-lg"><em>Let's setup one for now, you can setup more later!</em></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 max-w-4xl mx-auto">
|
|
||||||
<%= link_to my_wakatime_setup_step_3_path(editor: "vscode"), class: "bg-dark rounded-lg p-6 hover:bg-darkless transition-colors flex flex-col items-center justify-center text-center gap-4 text-white no-underline group h-full" do %>
|
|
||||||
<div class="w-20 h-20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
|
||||||
<img src="/images/editor-icons/vs-code-128.png" alt="VS Code" class="w-16 h-16 object-contain">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg">VS Code</h3>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to my_wakatime_setup_step_3_path(editor: "vim"), class: "bg-dark rounded-lg p-6 hover:bg-darkless transition-colors flex flex-col items-center justify-center text-center gap-4 text-white no-underline group h-full" do %>
|
|
||||||
<div class="w-20 h-20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
|
||||||
<img src="/images/editor-icons/vim-128.png" alt="Vim" class="w-16 h-16 object-contain">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg">Vim</h3>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to my_wakatime_setup_step_3_path(editor: "neovim"), class: "bg-dark rounded-lg p-6 hover:bg-darkless transition-colors flex flex-col items-center justify-center text-center gap-4 text-white no-underline group h-full" do %>
|
|
||||||
<div class="w-20 h-20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
|
||||||
<img src="/images/editor-icons/neovim-128.png" alt="Neovim" class="w-16 h-16 object-contain">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg">Neovim</h3>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to my_wakatime_setup_step_3_path(editor: "emacs"), class: "bg-dark rounded-lg p-6 hover:bg-darkless transition-colors flex flex-col items-center justify-center text-center gap-4 text-white no-underline group h-full" do %>
|
|
||||||
<div class="w-20 h-20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
|
||||||
<img src="/images/editor-icons/emacs-128.png" alt="Emacs" class="w-16 h-16 object-contain">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg">Emacs</h3>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to my_wakatime_setup_step_3_path(editor: "pycharm"), class: "bg-dark rounded-lg p-6 hover:bg-darkless transition-colors flex flex-col items-center justify-center text-center gap-4 text-white no-underline group h-full" do %>
|
|
||||||
<div class="w-20 h-20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
|
||||||
<img src="/images/editor-icons/pycharm-128.png" alt="PyCharm" class="w-16 h-16 object-contain">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg">PyCharm</h3>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to my_wakatime_setup_step_3_path(editor: "sublime"), class: "bg-dark rounded-lg p-6 hover:bg-darkless transition-colors flex flex-col items-center justify-center text-center gap-4 text-white no-underline group h-full" do %>
|
|
||||||
<div class="w-20 h-20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
|
||||||
<img src="/images/editor-icons/sublime-text-128.png" alt="Sublime" class="w-16 h-16 object-contain">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg">Sublime Text</h3>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to my_wakatime_setup_step_3_path(editor: "unity"), class: "bg-dark rounded-lg p-6 hover:bg-darkless transition-colors flex flex-col items-center justify-center text-center gap-4 text-white no-underline group h-full" do %>
|
|
||||||
<div class="w-20 h-20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
|
||||||
<img src="/images/editor-icons/unity-128.png" alt="Unity" class="w-16 h-16 object-contain">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg">Unity</h3>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to my_wakatime_setup_step_3_path(editor: "godot"), class: "bg-dark rounded-lg p-6 hover:bg-darkless transition-colors flex flex-col items-center justify-center text-center gap-4 text-white no-underline group h-full" do %>
|
|
||||||
<div class="w-20 h-20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
|
||||||
<img src="/images/editor-icons/godot-128.png" alt="Godot" class="w-16 h-16 object-contain">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg">Godot</h3>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= link_to my_wakatime_setup_step_3_path(editor: "other"), class: "bg-dark rounded-lg p-6 hover:bg-darkless transition-colors flex flex-col items-center justify-center text-center gap-4 text-white no-underline group h-full col-span-2 md:col-span-3 lg:col-span-4" do %>
|
|
||||||
<div class="w-20 h-20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform duration-200">
|
|
||||||
<div class="text-4xl">🔧</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg">Other Editor</h3>
|
|
||||||
<p class="text-secondary text-sm">Hackatime supports 70+ editors</p>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,377 +0,0 @@
|
||||||
<div class="min-h-screen text-white">
|
|
||||||
<div class="max-w-6xl mx-auto p-4">
|
|
||||||
<div class="text-center mb-4">
|
|
||||||
<h1 class="text-4xl font-bold text-primary mb-2">Hackatime Setup</h1>
|
|
||||||
<div class="flex items-center justify-center gap-2 mb-4">
|
|
||||||
<div class="w-8 h-8 bg-green rounded-full flex items-center justify-center text-sm font-bold">
|
|
||||||
<svg class="w-5 h-5 text-white" 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>
|
|
||||||
</div>
|
|
||||||
<div class="w-16 h-1 bg-green"></div>
|
|
||||||
<div class="w-8 h-8 bg-green rounded-full flex items-center justify-center text-sm font-bold">
|
|
||||||
<svg class="w-5 h-5 text-white" 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>
|
|
||||||
</div>
|
|
||||||
<div class="w-16 h-1 bg-green"></div>
|
|
||||||
<div class="w-8 h-8 bg-primary rounded-full flex items-center justify-center text-sm font-bold">3</div>
|
|
||||||
<div class="w-16 h-1 bg-darkless"></div>
|
|
||||||
<div class="w-8 h-8 bg-darkless rounded-full flex items-center justify-center text-sm">4</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if params[:editor] == "vscode" %>
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
<div class="lg:col-span-2">
|
|
||||||
<section class="bg-dark rounded-lg p-6">
|
|
||||||
<h3 class="text-2xl font-bold text-blue mb-4">💻 Install the VS Code Extension</h3>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<p class="text-lg">Install the <a href="https://marketplace.visualstudio.com/items?itemName=WakaTime.vscode-wakatime" target="_blank" rel="noopener noreferrer" class="text-cyan hover:text-blue underline font-semibold">WakaTime extension from the marketplace</a>.</p>
|
|
||||||
|
|
||||||
<h4 class="font-bold mb-2">Step-by-step:</h4>
|
|
||||||
<ol class="list-decimal list-inside space-y-1">
|
|
||||||
<li>Open VS Code</li>
|
|
||||||
<li>Click the Extensions icon (squares) on the left sidebar</li>
|
|
||||||
<li>Search for "WakaTime"</li>
|
|
||||||
<li>Click <strong>Install</strong> on the WakaTime extension</li>
|
|
||||||
<li>Restart VS Code if prompted</li>
|
|
||||||
<li>Code for a few minutes to start tracking your time to make sure it's working</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<details class="group">
|
|
||||||
<summary class="cursor-pointer text-secondary hover:text-white flex items-center gap-2">
|
|
||||||
<svg class="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
How do I know it's working?
|
|
||||||
</summary>
|
|
||||||
<div class="mt-3 pl-6">
|
|
||||||
<p class="text-md mb-3">You'll see a clock icon in your status bar:</p>
|
|
||||||
<img src="https://hc-cdn.hel1.your-objectstorage.com/s/v3/95d2513ce4b0c1c147827d17ecb4c24540cd73cc_p.png" alt="WakaTime status bar in VS Code" class="max-w-full h-auto rounded-lg border border-darkless">
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details class="group">
|
|
||||||
<summary class="cursor-pointer text-secondary hover:text-white flex items-center gap-2">
|
|
||||||
<svg class="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
Why is the extension called WakaTime?
|
|
||||||
</summary>
|
|
||||||
<div class="mt-3 pl-6">
|
|
||||||
<p class="text-md mb-3">WakaTime is a popular time tracking service for developers. Hackatime is compatible with WakaTime’s API, so you can install WakaTime plugins or extensions in your editor or IDE and configure them to send their data to Hackatime instead of the WakaTime service.</p>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lg:col-span-1">
|
|
||||||
<div class="lg:sticky lg:top-4">
|
|
||||||
<section id="status-panel" class="bg-dark rounded-lg p-6 border-2 border-darkless">
|
|
||||||
<div id="waiting-state">
|
|
||||||
<div class="text-center mb-4">
|
|
||||||
<div class="w-16 h-16 mx-auto mb-3 rounded-full bg-blue/20 flex items-center justify-center">
|
|
||||||
<svg class="w-8 h-8 text-blue animate-pulse" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 class="text-xl font-bold text-white mb-2">Waiting for you to code...</h4>
|
|
||||||
<p class="text-secondary text-sm" id="status-message">Once you've installed the extension, open a file and start typing!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-darkless rounded-lg p-3">
|
|
||||||
<div class="flex items-center justify-center gap-2 text-secondary">
|
|
||||||
<div class="spin"></div>
|
|
||||||
<span class="text-sm" id="poll-status">Checking for heartbeats...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="success-state" class="hidden">
|
|
||||||
<div class="text-center mb-4">
|
|
||||||
<div class="w-16 h-16 mx-auto mb-3 rounded-full bg-green/20 flex items-center justify-center">
|
|
||||||
<svg class="w-8 h-8 text-green" 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>
|
|
||||||
</div>
|
|
||||||
<h4 class="text-xl font-bold text-green mb-2">Heartbeat detected <span id="heartbeat-time-ago"></span>!</h4>
|
|
||||||
<p class="text-secondary text-sm" id="success-message">Hackatime is tracking your coding. Nice work!</p>
|
|
||||||
<p id="editor-mismatch-message" class="hidden text-cyan text-sm mt-2"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= link_to my_wakatime_setup_step_4_path, class: "block w-full bg-primary hover:bg-red text-white text-center px-6 py-3 rounded-lg font-semibold transition-colors" do %>
|
|
||||||
Continue →
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<p class="text-center text-secondary text-xs mt-3">Already set up? <a href="<%= my_wakatime_setup_step_4_path %>" class="text-cyan hover:underline">Skip to finish</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% elsif params[:editor] == "vim" %>
|
|
||||||
<section id="vim" class="bg-dark rounded-lg p-6 mb-6">
|
|
||||||
<h3 class="text-2xl font-bold text-green mb-4">📟 Vim</h3>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<p class="text-lg">Install the WakaTime plugin using your preferred plugin manager:</p>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="bg-green/10 border border-green/30 rounded-lg p-4">
|
|
||||||
<h4 class="font-bold mb-2">Using vim-plug:</h4>
|
|
||||||
<pre class="bg-darkless rounded p-3 text-cyan text-sm overflow-x-auto"><code>Plug 'wakatime/vim-wakatime'</code></pre>
|
|
||||||
<p class="text-sm mt-2">Then run <code class="bg-darkless px-1 rounded text-cyan">:PlugInstall</code></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-green/10 border border-green/30 rounded-lg p-4">
|
|
||||||
<h4 class="font-bold mb-2">Using Vundle:</h4>
|
|
||||||
<pre class="bg-darkless rounded p-3 text-cyan text-sm overflow-x-auto"><code>Plugin 'wakatime/vim-wakatime'</code></pre>
|
|
||||||
<p class="text-sm mt-2">Then run <code class="bg-darkless px-1 rounded text-cyan">:PluginInstall</code></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-green/10 border border-green/30 rounded-lg p-4">
|
|
||||||
<h4 class="font-bold mb-2">Manual installation:</h4>
|
|
||||||
<pre class="bg-darkless rounded p-3 text-cyan text-sm overflow-x-auto"><code>cd ~/.vim/bundle
|
|
||||||
git clone https://github.com/wakatime/vim-wakatime.git</code></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="flex gap-4 flex-wrap justify-center">
|
|
||||||
<%= link_to my_wakatime_setup_step_4_path, class: "bg-primary hover:bg-red text-white px-6 py-3 rounded-lg font-semibold transition-colors" do %>
|
|
||||||
Next Step
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% elsif params[:editor] == "neovim" %>
|
|
||||||
<section id="neovim" class="bg-dark rounded-lg p-6 mb-6">
|
|
||||||
<h3 class="text-2xl font-bold text-green mb-4">📟 Neovim</h3>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<p class="text-lg">Install the WakaTime plugin using your preferred plugin manager:</p>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="bg-green/10 border border-green/30 rounded-lg p-4">
|
|
||||||
<h4 class="font-bold mb-2">Using lazy.nvim:</h4>
|
|
||||||
<pre class="bg-darkless rounded p-3 text-cyan text-sm overflow-x-auto"><code>{ "wakatime/vim-wakatime", lazy = false }</code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-green/10 border border-green/30 rounded-lg p-4">
|
|
||||||
<h4 class="font-bold mb-2">Using packer.nvim:</h4>
|
|
||||||
<pre class="bg-darkless rounded p-3 text-cyan text-sm overflow-x-auto"><code>use 'wakatime/vim-wakatime'</code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-green/10 border border-green/30 rounded-lg p-4">
|
|
||||||
<h4 class="font-bold mb-2">Using vim-plug:</h4>
|
|
||||||
<pre class="bg-darkless rounded p-3 text-cyan text-sm overflow-x-auto"><code>Plug 'wakatime/vim-wakatime'</code></pre>
|
|
||||||
<p class="text-sm mt-2">Then run <code class="bg-darkless px-1 rounded text-cyan">:PlugInstall</code></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="flex gap-4 flex-wrap justify-center">
|
|
||||||
<%= link_to my_wakatime_setup_step_4_path, class: "bg-primary hover:bg-red text-white px-6 py-3 rounded-lg font-semibold transition-colors" do %>
|
|
||||||
Next Step
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% elsif params[:editor] == "emacs" %>
|
|
||||||
<section id="emacs" class="bg-dark rounded-lg p-6 mb-6">
|
|
||||||
<h3 class="text-2xl font-bold text-purple mb-4">🔮 Emacs</h3>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<p class="text-lg">Install the WakaTime package using your preferred method:</p>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="bg-purple/10 border border-purple/30 rounded-lg p-4">
|
|
||||||
<h4 class="font-bold mb-2">Using MELPA:</h4>
|
|
||||||
<pre class="bg-darkless rounded p-3 text-cyan text-sm overflow-x-auto"><code>M-x package-install RET wakatime-mode RET</code></pre>
|
|
||||||
<p class="text-sm mt-2">Then add to your config: <code class="bg-darkless px-1 rounded text-cyan">(global-wakatime-mode)</code></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-purple/10 border border-purple/30 rounded-lg p-4">
|
|
||||||
<h4 class="font-bold mb-2">Using use-package:</h4>
|
|
||||||
<pre class="bg-darkless rounded p-3 text-cyan text-sm overflow-x-auto"><code>(use-package wakatime-mode
|
|
||||||
:ensure t
|
|
||||||
:config
|
|
||||||
(global-wakatime-mode))</code></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="flex gap-4 flex-wrap justify-center">
|
|
||||||
<%= link_to my_wakatime_setup_step_4_path, class: "bg-primary hover:bg-red text-white px-6 py-3 rounded-lg font-semibold transition-colors" do %>
|
|
||||||
Next Step
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% elsif params[:editor] == "godot" %>
|
|
||||||
<section id="godot" class="bg-dark rounded-lg p-6 mb-6">
|
|
||||||
<h3 class="text-2xl font-bold text-cyan mb-4">🎮 Godot</h3>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<p class="text-lg">Follow our comprehensive <a href="/docs/editors/godot" class="text-cyan hover:text-blue underline">Godot & Hackatime Setup guide</a> with video tutorial!</p>
|
|
||||||
|
|
||||||
<div class="bg-cyan/10 border border-cyan/30 rounded-lg p-4">
|
|
||||||
<h4 class="font-bold mb-2">Quick steps:</h4>
|
|
||||||
<ol class="list-decimal list-inside space-y-1 text-sm">
|
|
||||||
<li>Open your Godot project</li>
|
|
||||||
<li>Go to the <strong>AssetLib</strong> tab</li>
|
|
||||||
<li>Search for <strong>"Godot Super Wakatime"</strong></li>
|
|
||||||
<li>Click <strong>Download</strong> and <strong>Install</strong></li>
|
|
||||||
<li>Enable in <strong>Project → Project Settings → Plugins</strong></li>
|
|
||||||
</ol>
|
|
||||||
<p class="text-sm mt-3 text-cyan">📺 <a href="https://www.youtube.com/watch?v=a938RgsBzNg&t=29s" target="_blank" class="underline">Watch the workshop recording</a> for a complete walkthrough!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-yellow/10 border border-yellow/30 rounded-lg p-4">
|
|
||||||
<p class="text-yellow text-sm"><strong>Note:</strong> You need to install the plugin for each Godot project separately (it's a Godot limitation).</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="flex gap-4 flex-wrap justify-center">
|
|
||||||
<%= link_to my_wakatime_setup_step_4_path, class: "bg-primary hover:bg-red text-white px-6 py-3 rounded-lg font-semibold transition-colors" do %>
|
|
||||||
Next Step
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% else %>
|
|
||||||
<section class="bg-dark rounded-lg p-6 mb-6">
|
|
||||||
<h3 class="text-2xl font-bold text-orange mb-4">🔧 Setup your Editor</h3>
|
|
||||||
<div class="bg-orange/10 border border-orange/30 rounded-lg p-4 mb-4">
|
|
||||||
<p class="mb-4"><strong>Hackatime works with any editor that supports WakaTime!</strong> This includes PyCharm, IntelliJ, Sublime Text, Atom, Neovim, Unity, Godot, and <a href="https://hackatime.hackclub.com/docs#supported-editors" class="text-cyan hover:text-blue underline">77+ more editors</a>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 class="font-bold mb-2 text-lg">Popular Editors:</h4>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
||||||
<a href="/docs/editors/pycharm" class="flex items-center gap-3 bg-darkless rounded-lg p-3 hover:bg-primary/75 transition-colors">
|
|
||||||
<img src="/images/editor-icons/pycharm-128.png" alt="PyCharm" class="w-8 h-8">
|
|
||||||
<span>PyCharm</span>
|
|
||||||
</a>
|
|
||||||
<a href="/docs/editors/sublime-text" class="flex items-center gap-3 bg-darkless rounded-lg p-3 hover:bg-primary/75 transition-colors">
|
|
||||||
<img src="/images/editor-icons/sublime-text-128.png" alt="Sublime Text" class="w-8 h-8">
|
|
||||||
<span>Sublime Text</span>
|
|
||||||
</a>
|
|
||||||
<a href="/docs/editors/unity" class="flex items-center gap-3 bg-darkless rounded-lg p-3 hover:bg-primary/75 transition-colors">
|
|
||||||
<img src="/images/editor-icons/unity-128.png" alt="Unity" class="w-8 h-8">
|
|
||||||
<span>Unity</span>
|
|
||||||
</a>
|
|
||||||
<a href="/docs/editors/neovim" class="flex items-center gap-3 bg-darkless rounded-lg p-3 hover:bg-primary/75 transition-colors">
|
|
||||||
<img src="/images/editor-icons/neovim-128.png" alt="Neovim" class="w-8 h-8">
|
|
||||||
<span>Neovim</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-yellow/10 border border-yellow/30 rounded-lg p-4">
|
|
||||||
<p class="text-yellow font-bold mb-2">⚠️ Important:</p>
|
|
||||||
<p>When setting up WakaTime plugins, <strong>skip any steps that ask you to update ~/.wakatime.cfg</strong> or change the api_url. The setup script from Step 1 already configured this correctly!</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="flex gap-4 flex-wrap justify-center">
|
|
||||||
<%= link_to my_wakatime_setup_step_4_path, class: "bg-primary hover:bg-red text-white px-6 py-3 rounded-lg font-semibold transition-colors" do %>
|
|
||||||
Next Step
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if params[:editor] == "vscode" %>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("turbo:load", function () {
|
|
||||||
const waitingState = document.getElementById("waiting-state");
|
|
||||||
const successState = document.getElementById("success-state");
|
|
||||||
const statusMessage = document.getElementById("status-message");
|
|
||||||
const pollStatus = document.getElementById("poll-status");
|
|
||||||
const statusPanel = document.getElementById("status-panel");
|
|
||||||
|
|
||||||
let checkCount = 0;
|
|
||||||
const maxChecks = 120; // 10m (5s)
|
|
||||||
|
|
||||||
const msg = ["Open any code file and start typing!", "Try editing some code in VS Code...", "Type a few characters in your editor!", "We're watching for your first keystroke...", "Make any edit in VS Code to continue!"];
|
|
||||||
|
|
||||||
function showSuccess(timeAgo, detectedEditor) {
|
|
||||||
waitingState.classList.add("hidden");
|
|
||||||
successState.classList.remove("hidden");
|
|
||||||
statusPanel.classList.remove("border-darkless");
|
|
||||||
statusPanel.classList.add("border-green");
|
|
||||||
document.getElementById("heartbeat-time-ago").textContent = timeAgo;
|
|
||||||
|
|
||||||
if (detectedEditor && detectedEditor.toLowerCase() !== "vscode" && detectedEditor.toLowerCase() !== "vs code") {
|
|
||||||
const mismatchMessage = document.getElementById("editor-mismatch-message");
|
|
||||||
mismatchMessage.textContent = `We detected a heartbeat from ${detectedEditor}. If this is intended, you're all set!`;
|
|
||||||
mismatchMessage.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function check() {
|
|
||||||
fetch(<%== api_v1_my_heartbeats_most_recent_path.to_json %>, {
|
|
||||||
headers: {
|
|
||||||
Authorization: "Bearer " + <%== @current_user_api_key.to_json %>,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.has_heartbeat) {
|
|
||||||
const heartbeatTime = new Date(data.heartbeat.created_at);
|
|
||||||
const now = new Date();
|
|
||||||
const secondsAgo = (now - heartbeatTime) / 1000;
|
|
||||||
const recentThreshold = 86400;
|
|
||||||
|
|
||||||
if (secondsAgo <= recentThreshold) {
|
|
||||||
showSuccess(data.time_ago, data.editor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error("No recent heartbeats");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
checkCount++;
|
|
||||||
|
|
||||||
if (checkCount % 3 === 0) {
|
|
||||||
const msgIndex = Math.floor(checkCount / 3) % msg.length;
|
|
||||||
statusMessage.textContent = msg[msgIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
pollStatus.textContent = `Checked ${checkCount} time${checkCount === 1 ? "" : "s"}...`;
|
|
||||||
|
|
||||||
if (checkCount >= maxChecks) {
|
|
||||||
pollStatus.textContent = "Still waiting... Make sure the extension is installed!";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(check, 5000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
check();
|
|
||||||
|
|
||||||
window.skipToNext = function () {
|
|
||||||
window.location.href = <%== my_wakatime_setup_step_4_path.to_json %>;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.spin {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid var(--color-darkless);
|
|
||||||
border-top: 2px solid var(--color-blue);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<% end %>
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
<div class="min-h-screen text-white">
|
|
||||||
<div class="max-w-6xl mx-auto p-4">
|
|
||||||
<div class="text-center mb-8">
|
|
||||||
<h1 class="text-4xl font-bold text-primary mb-2">Hackatime Setup</h1>
|
|
||||||
<div class="flex items-center justify-center gap-2 mb-4">
|
|
||||||
<div class="w-8 h-8 bg-green rounded-full flex items-center justify-center text-sm font-bold">
|
|
||||||
<svg class="w-5 h-5 text-white" 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>
|
|
||||||
</div>
|
|
||||||
<div class="w-16 h-1 bg-green"></div>
|
|
||||||
<div class="w-8 h-8 bg-green rounded-full flex items-center justify-center text-sm font-bold">
|
|
||||||
<svg class="w-5 h-5 text-white" 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>
|
|
||||||
</div>
|
|
||||||
<div class="w-16 h-1 bg-green"></div>
|
|
||||||
<div class="w-8 h-8 bg-green rounded-full flex items-center justify-center text-sm font-bold">
|
|
||||||
<svg class="w-5 h-5 text-white" 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>
|
|
||||||
</div>
|
|
||||||
<div class="w-16 h-1 bg-green"></div>
|
|
||||||
<div class="w-8 h-8 bg-green rounded-full flex items-center justify-center text-sm font-bold">
|
|
||||||
<svg class="w-5 h-5 text-white" 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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center mb-8 mt-4 max-w-5xl mx-auto">
|
|
||||||
<h2 class="text-3xl font-bold mt-4 mb-4">Oh, and one more thing...</h2>
|
|
||||||
<p class="text-xl text-white mb-2"><b>Please do not try to cheat the system!</b> We have measures in place to detect and prevent cheating. If you attempt to manipulate Hackatime, you will be banned from Hackatime and other participating YSWS / events / programs, so please play fair! We are a non-profit organization and we run off of donations.</p>
|
|
||||||
<label class="flex items-center justify-center gap-2 cursor-pointer select-none mt-2">
|
|
||||||
<input type="checkbox" id="o" class="w-5 h-5 rounded border-gray-300 text-primary focus:ring-primary bg-white">
|
|
||||||
<span class="text-xl text-primary">I agree and I understand the rules.</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<p class="text-xl text-white mt-2">But besides that, you're all set! Happy hacking!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center mb-8">
|
|
||||||
<div class="max-w-lg mx-auto">
|
|
||||||
<video src="<%= FlavorText.dino_meme_videos.sample %>" autoplay loop muted playsinline controls class="w-full rounded-lg"></video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-4 flex-wrap justify-center">
|
|
||||||
<%= link_to my_wakatime_setup_step_2_path, class: "px-4 py-3 bg-dark hover:bg-darkless border border-darkless text-gray-300 rounded transition-colors cursor-pointer flex items-center justify-center" do %>
|
|
||||||
Set up another editor
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if (url = session.dig(:return_data, "url")) %>
|
|
||||||
<%= link_to url, id: "s", class: "px-4 py-3 bg-primary hover:bg-primary/75 border border-darkless text-white rounded transition-colors cursor-pointer flex items-center justify-center opacity-50 cursor-not-allowed pointer-events-none" do %>
|
|
||||||
<%= session.dig(:return_data, 'button_text') || 'Done' %>
|
|
||||||
<% end %>
|
|
||||||
<% else %>
|
|
||||||
<%= link_to root_path, id: "s", class: "px-4 py-3 bg-primary hover:bg-primary/75 border border-darkless text-white rounded transition-colors cursor-pointer flex items-center justify-center opacity-50 cursor-not-allowed pointer-events-none" do %>
|
|
||||||
I agree, Get started!
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById("o").addEventListener("change", function () {
|
|
||||||
const x = document.getElementById("s");
|
|
||||||
if (this.checked) {
|
|
||||||
x.classList.remove("opacity-50", "cursor-not-allowed", "pointer-events-none");
|
|
||||||
x.disabled = false;
|
|
||||||
} else {
|
|
||||||
x.classList.add("opacity-50", "cursor-not-allowed", "pointer-events-none");
|
|
||||||
x.disabled = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
29
bin/dev
|
|
@ -1,18 +1,23 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
if ! gem list foreman -i --silent; then
|
export PORT="${PORT:-3000}"
|
||||||
|
|
||||||
|
if command -v overmind 1> /dev/null 2>&1
|
||||||
|
then
|
||||||
|
overmind start -f Procfile.dev "$@"
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v hivemind 1> /dev/null 2>&1
|
||||||
|
then
|
||||||
|
echo "Hivemind is installed. Running the application with Hivemind..."
|
||||||
|
exec hivemind Procfile.dev "$@"
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if gem list --no-installed --exact --silent foreman; then
|
||||||
echo "Installing foreman..."
|
echo "Installing foreman..."
|
||||||
gem install foreman
|
gem install foreman
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Default to port 3000 if not specified
|
foreman start -f Procfile.dev "$@"
|
||||||
export PORT="${PORT:-3000}"
|
|
||||||
|
|
||||||
# Let the debug gem allow remote connections,
|
|
||||||
# but avoid loading until `debugger` is called
|
|
||||||
export RUBY_DEBUG_OPEN="true"
|
|
||||||
export RUBY_DEBUG_LAZY="true"
|
|
||||||
|
|
||||||
# Use Procfile.dev, ensure web process binds to 0.0.0.0
|
|
||||||
# (Procfile.dev should have: web: bin/rails server -b 0.0.0.0)
|
|
||||||
exec foreman start -f Procfile.dev "$@"
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ FileUtils.chdir APP_ROOT do
|
||||||
|
|
||||||
puts "== Installing dependencies =="
|
puts "== Installing dependencies =="
|
||||||
system("bundle check") || system!("bundle install")
|
system("bundle check") || system!("bundle install")
|
||||||
|
system! "npm install"
|
||||||
|
|
||||||
# puts "\n== Copying sample files =="
|
# puts "\n== Copying sample files =="
|
||||||
# unless File.exist?("config/database.yml")
|
# unless File.exist?("config/database.yml")
|
||||||
|
|
|
||||||
16
bin/vite
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
#
|
||||||
|
# This file was generated by Bundler.
|
||||||
|
#
|
||||||
|
# The application 'vite' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
||||||
|
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
load Gem.bin_path("vite_ruby", "vite")
|
||||||
610
bun.lock
Normal file
|
|
@ -0,0 +1,610 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/inter": "^5.2.8",
|
||||||
|
"@inertiajs/svelte": "^2.3.13",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@tsconfig/svelte": "5",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"layerchart": "^1.0.13",
|
||||||
|
"plur": "^6.0.0",
|
||||||
|
"svelte": "5",
|
||||||
|
"svelte-check": "^4.3.6",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.3.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
|
"@dagrejs/dagre": ["@dagrejs/dagre@1.1.8", "", { "dependencies": { "@dagrejs/graphlib": "2.2.4" } }, "sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw=="],
|
||||||
|
|
||||||
|
"@dagrejs/graphlib": ["@dagrejs/graphlib@2.2.4", "", {}, "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||||
|
|
||||||
|
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
|
||||||
|
|
||||||
|
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
|
||||||
|
|
||||||
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||||
|
|
||||||
|
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||||
|
|
||||||
|
"@inertiajs/core": ["@inertiajs/core@2.3.13", "", { "dependencies": { "@types/lodash-es": "^4.17.12", "axios": "^1.13.2", "laravel-precognition": "^1.0.1", "lodash-es": "^4.17.23", "qs": "^6.14.1" } }, "sha512-qMHRnb59k/HehXw/WfQt5kPV0k9RapfFcWJZINJnYMwfHDEJ21iNVZjsJHmDN7yWdZmG1Dxi9FP4xarWWgdosQ=="],
|
||||||
|
|
||||||
|
"@inertiajs/svelte": ["@inertiajs/svelte@2.3.13", "", { "dependencies": { "@inertiajs/core": "2.3.13", "@types/lodash-es": "^4.17.12", "laravel-precognition": "^1.0.1", "lodash-es": "^4.17.23" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0" } }, "sha512-tKqAGn3FCdLA57bmZjm+26exVjZVQ0I15/KuoEofZKjZ8/4bndyHhhx79jmelZKlDNj4O3ECz15L5mHfo7YPSQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||||
|
|
||||||
|
"@layerstack/svelte-actions": ["@layerstack/svelte-actions@1.0.1", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@layerstack/utils": "1.0.1", "d3-array": "^3.2.4", "d3-scale": "^4.0.2", "date-fns": "^4.1.0", "lodash-es": "^4.17.21" } }, "sha512-Tv8B3TeT7oaghx0R0I4avnSdfAT6GxEK+StL8k/hEaa009iNOIGFl3f76kfvNvPioQHAMFGtnWGLPHfsfD41nQ=="],
|
||||||
|
|
||||||
|
"@layerstack/svelte-stores": ["@layerstack/svelte-stores@1.0.2", "", { "dependencies": { "@layerstack/utils": "1.0.1", "d3-array": "^3.2.4", "date-fns": "^4.1.0", "immer": "^10.1.1", "lodash-es": "^4.17.21", "zod": "^3.24.2" } }, "sha512-IxK0UKD0PVxg1VsyaR+n7NyJ+NlvyqvYYAp+J10lkjDQxm0yx58CaF2LBV08T22C3aY1iTlqJaatn/VHV4SoQg=="],
|
||||||
|
|
||||||
|
"@layerstack/tailwind": ["@layerstack/tailwind@1.0.1", "", { "dependencies": { "@layerstack/utils": "^1.0.1", "clsx": "^2.1.1", "culori": "^4.0.1", "d3-array": "^3.2.4", "date-fns": "^4.1.0", "lodash-es": "^4.17.21", "tailwind-merge": "^2.5.4", "tailwindcss": "^3.4.15" } }, "sha512-nlshEkUCfaV0zYzrFXVVYRnS8bnBjs4M7iui6l/tu6NeBBlxDivIyRraJkdYGCSL1lZHi6FqacLQ3eerHtz90A=="],
|
||||||
|
|
||||||
|
"@layerstack/utils": ["@layerstack/utils@1.0.1", "", { "dependencies": { "d3-array": "^3.2.4", "date-fns": "^4.1.0", "lodash-es": "^4.17.21" } }, "sha512-sWP9b+SFMkJYMZyYFI01aLxbg2ZUrix6Tv+BCDmeOrcLNxtWFsMYAomMhALzTMHbb+Vis/ua5vXhpdNXEw8a2Q=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
|
||||||
|
|
||||||
|
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="],
|
||||||
|
|
||||||
|
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||||
|
|
||||||
|
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
|
||||||
|
|
||||||
|
"@tailwindcss/forms": ["@tailwindcss/forms@0.5.11", "", { "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
||||||
|
|
||||||
|
"@tsconfig/svelte": ["@tsconfig/svelte@5.0.7", "", {}, "sha512-NOtJF9LQnV7k6bpzcXwL/rXdlFHvAT9e0imrftiMc6/+FUNBHRZ8UngDrM+jciA6ENzFYNoFs8rfwumuGF+Dhw=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="],
|
||||||
|
|
||||||
|
"@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
|
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
||||||
|
|
||||||
|
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||||
|
|
||||||
|
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||||
|
|
||||||
|
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
|
"axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="],
|
||||||
|
|
||||||
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
|
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||||
|
|
||||||
|
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
|
"commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||||
|
|
||||||
|
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
|
|
||||||
|
"culori": ["culori@4.0.2", "", {}, "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw=="],
|
||||||
|
|
||||||
|
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||||
|
|
||||||
|
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||||
|
|
||||||
|
"d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
|
||||||
|
|
||||||
|
"d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
|
||||||
|
|
||||||
|
"d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
|
||||||
|
|
||||||
|
"d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
|
||||||
|
|
||||||
|
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||||
|
|
||||||
|
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
|
||||||
|
|
||||||
|
"d3-geo-voronoi": ["d3-geo-voronoi@2.1.0", "", { "dependencies": { "d3-array": "3", "d3-delaunay": "6", "d3-geo": "3", "d3-tricontour": "1" } }, "sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q=="],
|
||||||
|
|
||||||
|
"d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
||||||
|
|
||||||
|
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||||
|
|
||||||
|
"d3-interpolate-path": ["d3-interpolate-path@2.3.0", "", {}, "sha512-tZYtGXxBmbgHsIc9Wms6LS5u4w6KbP8C09a4/ZYc4KLMYYqub57rRBUgpUr2CIarIrJEpdAWWxWQvofgaMpbKQ=="],
|
||||||
|
|
||||||
|
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||||
|
|
||||||
|
"d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
|
||||||
|
|
||||||
|
"d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
|
||||||
|
|
||||||
|
"d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
|
||||||
|
|
||||||
|
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||||
|
|
||||||
|
"d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
|
||||||
|
|
||||||
|
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||||
|
|
||||||
|
"d3-tile": ["d3-tile@1.0.0", "", {}, "sha512-79fnTKpPMPDS5xQ0xuS9ir0165NEwwkFpe/DSOmc2Gl9ldYzKKRDWogmTTE8wAJ8NA7PMapNfEcyKhI9Lxdu5Q=="],
|
||||||
|
|
||||||
|
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||||
|
|
||||||
|
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||||
|
|
||||||
|
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||||
|
|
||||||
|
"d3-tricontour": ["d3-tricontour@1.1.0", "", { "dependencies": { "d3-delaunay": "6", "d3-scale": "4" } }, "sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ=="],
|
||||||
|
|
||||||
|
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||||
|
|
||||||
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
|
|
||||||
|
"delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="],
|
||||||
|
|
||||||
|
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
|
||||||
|
|
||||||
|
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||||
|
|
||||||
|
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||||
|
|
||||||
|
"esrap": ["esrap@2.2.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ=="],
|
||||||
|
|
||||||
|
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
|
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
|
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
|
||||||
|
|
||||||
|
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||||
|
|
||||||
|
"irregular-plurals": ["irregular-plurals@4.2.0", "", {}, "sha512-bW9UXHL7bnUcNtTo+9ccSngbxc+V40H32IgvdVin0Xs8gbo+AVYD5g/72ce/54Kjfhq66vcZr8H8TKEvsifeOw=="],
|
||||||
|
|
||||||
|
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||||
|
|
||||||
|
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||||
|
|
||||||
|
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"laravel-precognition": ["laravel-precognition@1.0.2", "", { "dependencies": { "axios": "^1.4.0", "lodash-es": "^4.17.21" } }, "sha512-0H08JDdMWONrL/N314fvsO3FATJwGGlFKGkMF3nNmizVFJaWs17816iM+sX7Rp8d5hUjYCx6WLfsehSKfaTxjg=="],
|
||||||
|
|
||||||
|
"layercake": ["layercake@8.4.3", "", { "dependencies": { "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0" }, "peerDependencies": { "svelte": "3 - 5 || >=5.0.0-next.120", "typescript": "^5.0.2" } }, "sha512-PZDduaPFxgHHkxlmsz5MVBECf6ZCT39DI3LgMVvuMwrmlrtlXwXUM/elJp46zHYzCE1j+cGyDuBDxnANv94tOQ=="],
|
||||||
|
|
||||||
|
"layerchart": ["layerchart@1.0.13", "", { "dependencies": { "@dagrejs/dagre": "^1.1.4", "@layerstack/svelte-actions": "^1.0.1", "@layerstack/svelte-stores": "^1.0.2", "@layerstack/tailwind": "^1.0.1", "@layerstack/utils": "^1.0.1", "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-delaunay": "^6.0.4", "d3-dsv": "^3.0.1", "d3-force": "^3.0.0", "d3-geo": "^3.1.1", "d3-geo-voronoi": "^2.1.0", "d3-hierarchy": "^3.1.2", "d3-interpolate": "^3.0.1", "d3-interpolate-path": "^2.3.0", "d3-path": "^3.1.0", "d3-quadtree": "^3.0.1", "d3-random": "^3.0.1", "d3-sankey": "^0.12.3", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.1.0", "d3-shape": "^3.2.0", "d3-tile": "^1.0.0", "d3-time": "^3.1.0", "date-fns": "^4.1.0", "layercake": "8.4.3", "lodash-es": "^4.17.21" }, "peerDependencies": { "svelte": "^3.56.0 || ^4.0.0 || ^5.0.0" } }, "sha512-bjcrfyTdHtfYZn7yj26dvA1qUjM+R6+akp2VeBJ4JWKmDGhb5WvT9nMCs52Rb+gSd/omFq5SjZLz49MqlVljZw=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
|
||||||
|
|
||||||
|
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||||
|
|
||||||
|
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
|
|
||||||
|
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||||
|
|
||||||
|
"lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
|
|
||||||
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="],
|
||||||
|
|
||||||
|
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||||
|
|
||||||
|
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||||
|
|
||||||
|
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
|
||||||
|
|
||||||
|
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||||
|
|
||||||
|
"plur": ["plur@6.0.0", "", { "dependencies": { "irregular-plurals": "^4.2.0" } }, "sha512-Y9wXQivjRX0REtwpA9+n0bYYypWESn3cWtW2vazymw711qn+AQXxzZjRqhANYGBLIMC1UzVdpwe/1hHQwHfwng=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
|
||||||
|
|
||||||
|
"postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="],
|
||||||
|
|
||||||
|
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
|
||||||
|
|
||||||
|
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
|
||||||
|
|
||||||
|
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
|
||||||
|
|
||||||
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
|
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
|
||||||
|
|
||||||
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
|
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||||
|
|
||||||
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
|
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
|
||||||
|
|
||||||
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
|
|
||||||
|
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||||
|
|
||||||
|
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
|
||||||
|
|
||||||
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
|
"svelte": ["svelte@5.49.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-PYLwnngYzyhKzqDlGVlCH4z+NVI8mC0/bTv15vw25CcdOhxENsOHIbQ36oj5DIf3oBazM+STbCAvaskpxtBmWA=="],
|
||||||
|
|
||||||
|
"svelte-check": ["svelte-check@4.3.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@2.6.1", "", {}, "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||||
|
|
||||||
|
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
|
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||||
|
|
||||||
|
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||||
|
|
||||||
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
|
"@layerstack/tailwind/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
||||||
|
|
||||||
|
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
|
||||||
|
|
||||||
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||||
|
|
||||||
|
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||||
|
|
||||||
|
"@layerstack/tailwind/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||||
|
|
||||||
|
"@layerstack/tailwind/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||||
|
|
||||||
|
"@layerstack/tailwind/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||||
|
|
||||||
|
"d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
||||||
|
|
||||||
|
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
||||||
|
|
||||||
|
"@layerstack/tailwind/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"@layerstack/tailwind/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
|
"@layerstack/tailwind/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,7 +51,6 @@ module Harbor
|
||||||
secure: Rails.env.production?,
|
secure: Rails.env.production?,
|
||||||
httponly: true
|
httponly: true
|
||||||
|
|
||||||
config.middleware.use HtmlCompressor::Rack
|
|
||||||
config.middleware.use Rack::Attack
|
config.middleware.use Rack::Attack
|
||||||
config.exceptions_app = routes
|
config.exceptions_app = routes
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Pin npm packages by running ./bin/importmap
|
# Pin npm packages by running ./bin/importmap
|
||||||
|
|
||||||
pin "application"
|
pin "application"
|
||||||
|
pin "inertia_app"
|
||||||
pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
||||||
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
||||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,16 @@
|
||||||
# policy.img_src :self, :https, :data
|
# policy.img_src :self, :https, :data
|
||||||
# policy.object_src :none
|
# policy.object_src :none
|
||||||
# policy.script_src :self, :https
|
# policy.script_src :self, :https
|
||||||
|
# Allow @vite/client to hot reload javascript changes in development
|
||||||
|
# policy.script_src *policy.script_src, :unsafe_eval, "http://#{ ViteRuby.config.host_with_port }" if Rails.env.development?
|
||||||
|
|
||||||
|
# You may need to enable this in production as well depending on your setup.
|
||||||
|
# policy.script_src *policy.script_src, :blob if Rails.env.test?
|
||||||
|
|
||||||
# policy.style_src :self, :https
|
# policy.style_src :self, :https
|
||||||
|
# Allow @vite/client to hot reload style changes in development
|
||||||
|
# policy.style_src *policy.style_src, :unsafe_inline if Rails.env.development?
|
||||||
|
|
||||||
# # Specify URI for violation reports
|
# # Specify URI for violation reports
|
||||||
# # policy.report_uri "/csp-violation-report-endpoint"
|
# # policy.report_uri "/csp-violation-report-endpoint"
|
||||||
# end
|
# end
|
||||||
|
|
|
||||||
|
|
@ -48,20 +48,20 @@ Rails.application.configure do
|
||||||
cron: "0 0 * * *",
|
cron: "0 0 * * *",
|
||||||
class: "SlackUsernameUpdateJob"
|
class: "SlackUsernameUpdateJob"
|
||||||
},
|
},
|
||||||
# scan_github_repos: {
|
scan_github_repos: {
|
||||||
# cron: "0 10 * * *",
|
cron: "0 10 * * *",
|
||||||
# class: "ScanGithubReposJob"
|
class: "ScanGithubReposJob"
|
||||||
# },
|
},
|
||||||
# sync_all_user_repo_events: {
|
sync_all_user_repo_events: {
|
||||||
# cron: "0 */6 * * *", # Every 6 hours (at minute 0 of 0, 6, 12, 18 hours)
|
cron: "0 */6 * * *", # Every 6 hours (at minute 0 of 0, 6, 12, 18 hours)
|
||||||
# class: "SyncAllUserRepoEventsJob",
|
class: "SyncAllUserRepoEventsJob",
|
||||||
# description: "Periodically syncs repository events for all eligible users."
|
description: "Periodically syncs repository events for all eligible users."
|
||||||
# },
|
},
|
||||||
# scan_repo_events_for_commits: {
|
scan_repo_events_for_commits: {
|
||||||
# cron: "0 */3 * * *", # Every 3 hours at minute 0
|
cron: "0 */3 * * *", # Every 3 hours at minute 0
|
||||||
# class: "ScanRepoEventsForCommitsJob",
|
class: "ScanRepoEventsForCommitsJob",
|
||||||
# description: "Scans repository host events (PushEvents) and enqueues jobs to process new commits."
|
description: "Scans repository host events (PushEvents) and enqueues jobs to process new commits."
|
||||||
# },
|
},
|
||||||
# cleanup_expired_email_verification_requests: {
|
# cleanup_expired_email_verification_requests: {
|
||||||
# cron: "* * * * *",
|
# cron: "* * * * *",
|
||||||
# class: "CleanupExpiredEmailVerificationRequestsJob"
|
# class: "CleanupExpiredEmailVerificationRequestsJob"
|
||||||
|
|
|
||||||
9
config/initializers/inertia_rails.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
InertiaRails.configure do |config|
|
||||||
|
config.version = ViteRuby.digest
|
||||||
|
config.encrypt_history = true
|
||||||
|
config.always_include_errors_hash = true
|
||||||
|
config.use_script_element_for_initial_page = true
|
||||||
|
config.use_data_inertia_head_attribute = true
|
||||||
|
end
|
||||||
|
|
@ -11,6 +11,11 @@ class AdminLevelConstraint
|
||||||
end
|
end
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
|
# Redirect to localhost from 127.0.0.1 to use same IP address with Vite server
|
||||||
|
constraints(host: "127.0.0.1") do
|
||||||
|
get "(*path)", to: redirect { |params, req| "#{req.protocol}localhost:#{req.port}/#{params[:path]}" }
|
||||||
|
end
|
||||||
|
|
||||||
mount Rswag::Api::Engine => "/api-docs"
|
mount Rswag::Api::Engine => "/api-docs"
|
||||||
mount Rswag::Ui::Engine => "/api-docs"
|
mount Rswag::Ui::Engine => "/api-docs"
|
||||||
use_doorkeeper
|
use_doorkeeper
|
||||||
|
|
@ -77,12 +82,10 @@ Rails.application.routes.draw do
|
||||||
resources :static_pages, only: [ :index ] do
|
resources :static_pages, only: [ :index ] do
|
||||||
collection do
|
collection do
|
||||||
get :project_durations
|
get :project_durations
|
||||||
get :activity_graph
|
|
||||||
get :currently_hacking
|
get :currently_hacking
|
||||||
get :currently_hacking_count
|
get :currently_hacking_count
|
||||||
get :filterable_dashboard_content
|
get :filterable_dashboard_content
|
||||||
get :filterable_dashboard
|
get :filterable_dashboard
|
||||||
get :mini_leaderboard
|
|
||||||
get :streak
|
get :streak
|
||||||
# get :timeline # Removed: Old route for timeline
|
# get :timeline # Removed: Old route for timeline
|
||||||
end
|
end
|
||||||
|
|
|
||||||
16
config/vite.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"all": {
|
||||||
|
"sourceCodeDir": "app/javascript",
|
||||||
|
"watchAdditionalPaths": []
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"autoBuild": true,
|
||||||
|
"publicOutputDir": "vite-dev",
|
||||||
|
"port": 3036
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"autoBuild": true,
|
||||||
|
"publicOutputDir": "vite-test",
|
||||||
|
"port": 3037
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- bundle_cache:/usr/local/bundle
|
- bundle_cache:/usr/local/bundle
|
||||||
|
- node_modules:/app/node_modules
|
||||||
environment:
|
environment:
|
||||||
- RAILS_ENV=development
|
- RAILS_ENV=development
|
||||||
- DATABASE_URL=postgres://postgres:secureorpheus123@db:5432/app_development
|
- DATABASE_URL=postgres://postgres:secureorpheus123@db:5432/app_development
|
||||||
|
|
@ -30,4 +31,5 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
harbor_postgres_data:
|
harbor_postgres_data:
|
||||||
bundle_cache:
|
bundle_cache:
|
||||||
|
node_modules:
|
||||||
BIN
docs-index.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
docs-vs-code.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
3652
package-lock.json
generated
Normal file
25
package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/inter": "^5.2.8",
|
||||||
|
"@inertiajs/svelte": "^2.3.13",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@tsconfig/svelte": "5",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"layerchart": "^1.0.13",
|
||||||
|
"plur": "^6.0.0",
|
||||||
|
"svelte": "5",
|
||||||
|
"svelte-check": "^4.3.6",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
try {
|
|
||||||
# Create config file with API settings
|
|
||||||
$configPath = "$env:USERPROFILE\.wakatime.cfg"
|
|
||||||
# If config exists, backup
|
|
||||||
if (Test-Path $configPath) {
|
|
||||||
Write-Host "[INFO] Config file already exists, moving into ~/.wakatime.cfg.bak"
|
|
||||||
Move-Item -Path $configPath -Destination "$configPath.bak"
|
|
||||||
}
|
|
||||||
|
|
||||||
@"
|
|
||||||
[settings]
|
|
||||||
api_url = $env:HACKATIME_API_URL
|
|
||||||
api_key = $env:HACKATIME_API_KEY
|
|
||||||
heartbeat_rate_limit_seconds = 30
|
|
||||||
"@ | Out-File -FilePath $configPath -Force -Encoding utf8
|
|
||||||
|
|
||||||
Write-Host "Config file created at $configPath"
|
|
||||||
|
|
||||||
# Verify config was created successfully
|
|
||||||
if (Test-Path $configPath) {
|
|
||||||
$config = Get-Content $configPath
|
|
||||||
$apiUrl = ($config | Select-String "api_url").ToString().Split('=')[1].Trim()
|
|
||||||
$apiKey = ($config | Select-String "api_key").ToString().Split('=')[1].Trim()
|
|
||||||
$heartbeatRate = ($config | Select-String "heartbeat_rate_limit_seconds").ToString().Split('=')[1].Trim()
|
|
||||||
|
|
||||||
# Display verification info
|
|
||||||
Write-Host "API URL: $apiUrl"
|
|
||||||
Write-Host ("API Key: " + $apiKey.Substring(0,4) + "..." + $apiKey.Substring($apiKey.Length-4)) # Show first/last 4 chars
|
|
||||||
|
|
||||||
# Send test heartbeat
|
|
||||||
Write-Host "Sending test heartbeat..."
|
|
||||||
$time = [Math]::Floor([decimal](Get-Date(Get-Date).ToUniversalTime()-uformat '%s'))
|
|
||||||
$heartbeat = @{
|
|
||||||
type = 'file'
|
|
||||||
time = $time
|
|
||||||
entity = 'test.txt'
|
|
||||||
language = 'Text'
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = Invoke-RestMethod -Uri "$apiUrl/users/current/heartbeats" `
|
|
||||||
-Method Post `
|
|
||||||
-Headers @{Authorization="Bearer $apiKey"} `
|
|
||||||
-ContentType 'application/json' `
|
|
||||||
-Body "[$($heartbeat | ConvertTo-Json)]"
|
|
||||||
|
|
||||||
Write-Host "Test heartbeat sent successfully"
|
|
||||||
|
|
||||||
# Display ASCII art on success
|
|
||||||
Write-Host "`n" -NoNewline
|
|
||||||
Write-Host " _ _ _ _ _ " -ForegroundColor Cyan
|
|
||||||
Write-Host " | | | | __ _ ___| | ____ _| |_(_)_ __ ___ ___ " -ForegroundColor Cyan
|
|
||||||
Write-Host " | |_| |/ _` |/ __| |/ / _` | __| | '_ ` _ \ / _ \" -ForegroundColor Cyan
|
|
||||||
Write-Host " | _ | (_| | (__| < (_| | |_| | | | | | | __/" -ForegroundColor Cyan
|
|
||||||
Write-Host " |_| |_|\__,_|\___|_|\_\__,_|\__|_|_| |_| |_|\___|" -ForegroundColor Cyan
|
|
||||||
Write-Host "`n Ready to track! " -ForegroundColor Green
|
|
||||||
Write-Host " https://hackatime.hackclub.com " -ForegroundColor Yellow
|
|
||||||
Write-Host "`n"
|
|
||||||
} else {
|
|
||||||
throw "Failed to create config file"
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Host "----------------------------------------"
|
|
||||||
Write-Host "ERROR: An error occurred during setup:" -ForegroundColor Red
|
|
||||||
Write-Host $_.Exception.Message -ForegroundColor Red
|
|
||||||
Write-Host "----------------------------------------"
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Write-Host "Press any key to exit..."
|
|
||||||
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# If config exists, backup
|
|
||||||
if [ -f ~/.wakatime.cfg ]; then
|
|
||||||
echo "[INFO] Config file already exists, moving into ~/.wakatime.cfg.bak"
|
|
||||||
mv ~/.wakatime.cfg ~/.wakatime.cfg.bak
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create or update config file
|
|
||||||
cat > ~/.wakatime.cfg << EOL
|
|
||||||
[settings]
|
|
||||||
api_url = ${HACKATIME_API_URL}
|
|
||||||
api_key = ${HACKATIME_API_KEY}
|
|
||||||
heartbeat_rate_limit_seconds = 30
|
|
||||||
EOL
|
|
||||||
|
|
||||||
echo "Config file created at ~/.wakatime.cfg"
|
|
||||||
|
|
||||||
# Read values from config to verify
|
|
||||||
if [ ! -f ~/.wakatime.cfg ]; then
|
|
||||||
echo "Error: Config file not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
API_URL=$(sed -n 's/.*api_url = \(.*\)/\1/p' ~/.wakatime.cfg)
|
|
||||||
API_KEY=$(sed -n 's/.*api_key = \(.*\)/\1/p' ~/.wakatime.cfg)
|
|
||||||
HEARTBEAT_RATE_LIMIT=$(sed -n 's/.*heartbeat_rate_limit_seconds = \(.*\)/\1/p' ~/.wakatime.cfg)
|
|
||||||
|
|
||||||
if [ -z "$API_URL" ] || [ -z "$API_KEY" ] || [ -z "$HEARTBEAT_RATE_LIMIT" ]; then
|
|
||||||
echo "Error: Could not read api_url, api_key, or heartbeat_rate_limit_seconds from config"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Successfully read config:"
|
|
||||||
echo "API URL: $API_URL"
|
|
||||||
echo "API Key: ${API_KEY:0:8}..." # Show only first 8 chars for security
|
|
||||||
|
|
||||||
# Send test heartbeat using values from config
|
|
||||||
echo "Sending test heartbeat..."
|
|
||||||
response=$(curl -s -w "\n%{http_code}" -X POST "$API_URL/users/current/heartbeats" \
|
|
||||||
-H "Authorization: Bearer $API_KEY" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "[{\"type\":\"file\",\"time\":$(date +%s),\"entity\":\"test.txt\",\"language\":\"Text\"}]")
|
|
||||||
|
|
||||||
http_code=$(echo "$response" | tail -n1)
|
|
||||||
body=$(echo "$response" | sed '$d')
|
|
||||||
|
|
||||||
if [ "$http_code" = "200" ] || [ "$http_code" = "202" ]; then
|
|
||||||
curl "$SUCCESS_URL"
|
|
||||||
echo -e "\nTest heartbeat sent successfully"
|
|
||||||
else
|
|
||||||
echo -e "\nError sending heartbeat: $body"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
Before Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 329 KiB |
|
Before Width: | Height: | Size: 2 MiB |
|
Before Width: | Height: | Size: 2 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 592 KiB |
|
|
@ -1,7 +0,0 @@
|
||||||
_____ _
|
|
||||||
/ ____| | |
|
|
||||||
| (___ _ _ ___ ___ ___ ___ ___| |
|
|
||||||
\___ \| | | |/ __/ __/ _ \ __/ __| |
|
|
||||||
____) | |_| | (__ (__ __\__ \__ \_|
|
|
||||||
|_____/ \__,_|\___\___\___|___/___(_)
|
|
||||||
|
|
||||||
7
svelte.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
}
|
||||||
29
tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
/**
|
||||||
|
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||||
|
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||||
|
* Note that setting allowJs false does not prevent the use
|
||||||
|
* of JS in `.svelte` files.
|
||||||
|
*/
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
|
||||||
|
/* Aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["app/javascript/*"],
|
||||||
|
"~/*": ["app/javascript/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"include": ["app/javascript/**/*.ts", "app/javascript/**/*.js", "app/javascript/**/*.svelte"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
12
tsconfig.node.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
12
vite.config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import RubyPlugin from 'vite-plugin-ruby'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
svelte(),
|
||||||
|
tailwindcss(),
|
||||||
|
RubyPlugin(),
|
||||||
|
],
|
||||||
|
})
|
||||||