Remove unused ERBs, /wakatime-alternative (#976)

* Remove unused ERBs, /wakatime-alternative

* Update grid icons

* Fix classes
This commit is contained in:
Mahad Kalam 2026-02-18 06:44:27 +00:00 committed by GitHub
parent 32c97bbf1b
commit 3fbc925572
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 526 additions and 933 deletions

View file

@ -1,3 +1,8 @@
vendor/
swagger/
docs/
spec/fixtures/heartbeats.json
package-lock.json
tsconfig.json
tsconfig.*.json
db/migrate/

View file

@ -1,7 +1,7 @@
class StaticPagesController < InertiaController
include DashboardData
layout "inertia", only: :index
layout "inertia", only: %i[index wakatime_alternative]
def index
if current_user
@ -114,6 +114,18 @@ class StaticPagesController < InertiaController
render partial: "streak"
end
def wakatime_alternative
@page_title = "WakaTime Alternative - Free & Open Source Coding Time Tracker | Hackatime"
@meta_description = "Looking for a WakaTime alternative? Hackatime is a free, open source coding time tracker with all features unlocked. Compare features, pricing, and see why developers are switching."
@meta_keywords = "wakatime alternative, free time tracker, coding time tracker, open source wakatime, hackatime, developer analytics, programming stats"
@og_title = "WakaTime Alternative - Free & Open Source | Hackatime"
@og_description = @meta_description
@twitter_title = @og_title
@twitter_description = @meta_description
render inertia: "WakatimeAlternative"
end
private
def set_homepage_seo_content

View file

@ -0,0 +1,96 @@
<script lang="ts">
import { Link } from "@inertiajs/svelte";
</script>
<footer class="py-16 w-full bg-surface">
<div class="max-w-275 mx-auto px-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<div class="md:col-span-1">
<a href="/" class="flex items-center gap-2 mb-4">
<img
src="/images/new-icon-rounded.png"
class="w-8 h-8 rounded-lg"
alt="Hackatime"
/>
<span class="font-bold text-xl tracking-tight">Hackatime</span>
</a>
<p class="text-sm text-secondary max-w-[35ch] leading-relaxed">
A project by Hack Club. We build tools, community, and events for the
next generation of hackers.
</p>
</div>
<div>
<h5
class="text-xs uppercase tracking-wider text-muted font-semibold mb-4"
>
Platform
</h5>
<div class="space-y-3">
<Link
href="/docs"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Documentation</Link
>
<a
href="/leaderboards"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Leaderboards</a
>
<Link
href="/docs/editors/vs-code"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Editor Setup</Link
>
</div>
</div>
<div>
<h5
class="text-xs uppercase tracking-wider text-muted font-semibold mb-4"
>
Community
</h5>
<div class="space-y-3">
<a
href="https://hackclub.com/slack"
target="_blank"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Slack</a
>
<a
href="https://github.com/hackclub/hackatime"
target="_blank"
class="block text-sm text-secondary hover:text-primary transition-colors"
>GitHub</a
>
<a
href="https://hackclub.com"
target="_blank"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Hack Club</a
>
</div>
</div>
<div>
<h5
class="text-xs uppercase tracking-wider text-muted font-semibold mb-4"
>
Legal
</h5>
<div class="space-y-3">
<a
href="https://hackclub.com/privacy-and-terms"
target="_blank"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Privacy & Terms</a
>
<a
href="https://hackclub.com/conduct"
target="_blank"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Code of Conduct</a
>
</div>
</div>
</div>
</div>
</footer>

View file

@ -10,6 +10,7 @@
import HowItWorks from "./signedOut/HowItWorks.svelte";
import FAQSection from "./signedOut/FAQSection.svelte";
import CTASection from "./signedOut/CTASection.svelte";
import MarketingFooter from "../../components/MarketingFooter.svelte";
type HomeStats = { seconds_tracked?: number; users_tracked?: number };
@ -194,96 +195,5 @@
usersTracked={formatNumber(usersTracked)}
/>
<footer class="py-16 w-full bg-surface">
<div class="max-w-275 mx-auto px-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<div class="md:col-span-1">
<a href="/" class="flex items-center gap-2 mb-4">
<img
src="/images/new-icon-rounded.png"
class="w-8 h-8 rounded-lg"
alt="Hackatime"
/>
<span class="font-bold text-xl tracking-tight">Hackatime</span>
</a>
<p class="text-sm text-secondary max-w-[35ch] leading-relaxed">
A project by Hack Club. We build tools, community, and events for
the next generation of hackers.
</p>
</div>
<div>
<h5
class="text-xs uppercase tracking-wider text-muted font-semibold mb-4"
>
Platform
</h5>
<div class="space-y-3">
<Link
href="/docs"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Documentation</Link
>
<a
href="/leaderboards"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Leaderboards</a
>
<Link
href="/docs/editors/vs-code"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Editor Setup</Link
>
</div>
</div>
<div>
<h5
class="text-xs uppercase tracking-wider text-muted font-semibold mb-4"
>
Community
</h5>
<div class="space-y-3">
<a
href="https://hackclub.com/slack"
target="_blank"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Slack</a
>
<a
href="https://github.com/hackclub/hackatime"
target="_blank"
class="block text-sm text-secondary hover:text-primary transition-colors"
>GitHub</a
>
<a
href="https://hackclub.com"
target="_blank"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Hack Club</a
>
</div>
</div>
<div>
<h5
class="text-xs uppercase tracking-wider text-muted font-semibold mb-4"
>
Legal
</h5>
<div class="space-y-3">
<a
href="https://hackclub.com/privacy-and-terms"
target="_blank"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Privacy & Terms</a
>
<a
href="https://hackclub.com/conduct"
target="_blank"
class="block text-sm text-secondary hover:text-primary transition-colors"
>Code of Conduct</a
>
</div>
</div>
</div>
</div>
</footer>
<MarketingFooter />
</div>

View file

@ -4,37 +4,38 @@
title: "Time tracking",
description:
"Automatic tracking that starts when you type and stops when you take a break. Down to the second.",
icon: `<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />`,
icon: `<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zM12.75 6a.75.75 0 00-1.5 0v6c0 .414.336.75.75.75h4.5a.75.75 0 000-1.5h-3.75V6z" />`,
},
{
title: "Language stats",
description:
"See your actual tech stack. How much Rust vs. how much time debugging config files.",
icon: `<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />`,
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6"><path fill-rule="evenodd" d="M3 6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6Zm14.25 6a.75.75 0 0 1-.22.53l-2.25 2.25a.75.75 0 1 1-1.06-1.06L15.44 12l-1.72-1.72a.75.75 0 1 1 1.06-1.06l2.25 2.25c.141.14.22.331.22.53Zm-10.28-.53a.75.75 0 0 0 0 1.06l2.25 2.25a.75.75 0 1 0 1.06-1.06L8.56 12l1.72-1.72a.75.75 0 1 0-1.06-1.06l-2.25 2.25Z" clip-rule="evenodd" /></svg>`,
},
{
title: "File-level insights",
description:
"Drill into specific files. Find the ones consuming all your time.",
icon: `<path stroke-linecap="round" stroke-linejoin="round" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />`,
icon: `<path d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0016.5 9h-1.875a1.875 1.875 0 01-1.875-1.875V5.25A3.75 3.75 0 009 1.5H5.625z" /><path d="M12.971 1.816A5.23 5.23 0 0114.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 013.434 1.279 9.768 9.768 0 00-6.963-6.963z" /><path d="M9 12.75a.75.75 0 00-1.5 0v4.5a.75.75 0 001.5 0v-4.5zm2.25 2.25a.75.75 0 00-1.5 0v2.25a.75.75 0 001.5 0V15zm2.25-1.5a.75.75 0 00-1.5 0v3.75a.75.75 0 001.5 0v-3.75z" />`,
},
{
title: "Zero lock-in",
description:
"Your data is yours. Export your entire history to JSON or CSV via the REST API.",
icon: `<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />`,
icon: `<path fill-rule="evenodd" d="M9.75 6.75h-3a3 3 0 0 0-3 3v7.5a3 3 0 0 0 3 3h7.5a3 3 0 0 0 3-3v-7.5a3 3 0 0 0-3-3h-3V1.5a.75.75 0 0 0-1.5 0v5.25Zm0 0h1.5v5.69l1.72-1.72a.75.75 0 1 1 1.06 1.06l-3 3a.75.75 0 0 1-1.06 0l-3-3a.75.75 0 1 1 1.06-1.06l1.72 1.72V6.75Z" clip-rule="evenodd" />
<path d="M7.151 21.75a2.999 2.999 0 0 0 2.599 1.5h7.5a3 3 0 0 0 3-3v-7.5c0-1.11-.603-2.08-1.5-2.599v7.099a4.5 4.5 0 0 1-4.5 4.5H7.151Z" />`,
},
{
title: "Leaderboards (coming soon)",
description:
"Create private leaderboards for your team or hackathon. Compete on consistency.",
icon: `<path stroke-linecap="round" stroke-linejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />`,
icon: `<path d="M4.5 6.375a4.125 4.125 0 118.25 0 4.125 4.125 0 01-8.25 0zM14.25 8.625a3.375 3.375 0 116.75 0 3.375 3.375 0 01-6.75 0zM1.5 19.125a7.125 7.125 0 0114.25 0v.003l-.001.119a.75.75 0 01-.363.63 13.067 13.067 0 01-6.761 1.873c-2.472 0-4.786-.684-6.76-1.873a.75.75 0 01-.364-.63l-.001-.122zM17.25 19.128l-.001.144a2.25 2.25 0 01-.233.96 10.088 10.088 0 005.06-1.01.75.75 0 00.42-.643 4.875 4.875 0 00-6.957-4.611 8.586 8.586 0 011.71 5.157v.003z" />`,
},
{
title: "Privacy-first",
description:
"We don't sell your data. We don't use it for AI training. Just heartbeats and charts.",
icon: `<path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />`,
icon: `<path fill-rule="evenodd" clip-rule="evenodd" d="M12 1.5a5.25 5.25 0 00-5.25 5.25v3a3 3 0 00-3 3v6.75a3 3 0 003 3h10.5a3 3 0 003-3v-6.75a3 3 0 00-3-3v-3c0-2.9-2.35-5.25-5.25-5.25zm3.75 8.25v-3a3.75 3.75 0 10-7.5 0v3h7.5z" />`,
},
];
</script>
@ -47,7 +48,7 @@
>
Everything you need, nothing you don't.
</h2>
<p class="text-secondary text-lg">
<p class="text-secondary text-lg text-pretty">
Granular telemetry for your development environment, delivered via a
clean dashboard.
</p>
@ -57,10 +58,8 @@
<div class="p-8 bg-surface border border-surface-200 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="currentColor"
class="w-8 h-8 text-purple mb-4"
>
{@html feature.icon}

View file

@ -25,7 +25,8 @@
</h3>
<p class="text-secondary text-base leading-relaxed">
Other trackers delete your history after two weeks unless you pay.
Hackatime stores it indefinitely.
Hackatime stores it indefinitely, making it a perfect alternative to
WakaTime.
</p>
</div>
<div class="p-8 bg-darker border border-surface-200 rounded-lg">

View file

@ -0,0 +1,397 @@
<script module lang="ts">
export const layout = false;
</script>
<script lang="ts">
import { Link } from "@inertiajs/svelte";
import MarketingFooter from "../components/MarketingFooter.svelte";
let previousTheme = $state<string | null>(null);
$effect(() => {
const html = document.documentElement;
previousTheme = html.getAttribute("data-theme");
html.setAttribute("data-theme", "gruvbox_dark");
const colorSchemeMeta = document.querySelector("meta[name='color-scheme']");
colorSchemeMeta?.setAttribute("content", "dark");
return () => {
if (previousTheme) {
html.setAttribute("data-theme", previousTheme);
}
};
});
type Feature = {
name: string;
hackatime: string;
hackatimeHighlight: boolean;
wakatimeFree: string;
wakatimePro: string;
};
const features: Feature[] = [
{
name: "Price",
hackatime: "Free forever",
hackatimeHighlight: true,
wakatimeFree: "Free",
wakatimePro: "$9/month",
},
{
name: "Open Source",
hackatime: "\u2713",
hackatimeHighlight: true,
wakatimeFree: "\u2717",
wakatimePro: "\u2717",
},
{
name: "Editor Support",
hackatime: "70+ editors",
hackatimeHighlight: false,
wakatimeFree: "70+ editors",
wakatimePro: "70+ editors",
},
{
name: "Data Retention",
hackatime: "Unlimited",
hackatimeHighlight: true,
wakatimeFree: "14 days",
wakatimePro: "Unlimited",
},
{
name: "Project Stats",
hackatime: "Full access",
hackatimeHighlight: true,
wakatimeFree: "Limited",
wakatimePro: "Full access",
},
{
name: "Language Breakdown",
hackatime: "\u2713",
hackatimeHighlight: true,
wakatimeFree: "\u2713",
wakatimePro: "\u2713",
},
{
name: "Leaderboards",
hackatime: "\u2713 (Community)",
hackatimeHighlight: true,
wakatimeFree: "\u2713 (Community)",
wakatimePro: "\u2713",
},
{
name: "Self-Hosting",
hackatime: "\u2713",
hackatimeHighlight: true,
wakatimeFree: "\u2717",
wakatimePro: "\u2717",
},
{
name: "Team Features",
hackatime: "\u2717",
hackatimeHighlight: false,
wakatimeFree: "\u2717",
wakatimePro: "\u2713",
},
{
name: "API Access",
hackatime: "\u2713 Full",
hackatimeHighlight: true,
wakatimeFree: "Limited",
wakatimePro: "\u2713 Full",
},
];
type TradeOff = { title: string; description: string };
const tradeOffs: TradeOff[] = [
{
title: "No team dashboards",
description:
"WakaTime Pro offers team analytics. Hackatime is focused on individual developers. If you need team insights, WakaTime might be better suited.",
},
{
title: "Community-focused",
description:
"Hackatime is built for the Hack Club community. While anyone can use it, some features (like leaderboards) are community-centric.",
},
{
title: "Newer platform",
description:
"WakaTime has been around since 2013 and has a mature ecosystem. Hackatime launched in 2024 and is actively developed.",
},
{
title: "No email summaries",
description:
"WakaTime sends weekly email reports. Hackatime doesn't currently offer email digests, but this is a feature we're actively working on.",
},
];
const benefits = [
"100% free, forever\u2014every feature, no exceptions",
"Works with all WakaTime plugins\u2014just change your API endpoint",
"Open source\u2014audit the code, contribute, or self-host",
"Privacy-first\u2014only metadata tracked, never your code",
"Community leaderboards\u2014see how you stack up against peers",
];
</script>
<div class="min-h-screen w-full bg-darker text-surface-content">
<!-- Fixed Header -->
<header
class="fixed top-0 w-full bg-darker/95 backdrop-blur-sm z-50 border-b border-surface-200/60"
>
<div
class="max-w-[1100px] mx-auto px-6 py-4 flex justify-between items-center"
>
<a href="/" class="flex items-center gap-3">
<img
src="/images/new-icon-rounded.png"
class="w-10 h-10 rounded-lg"
alt="Hackatime"
/>
<span class="font-bold text-2xl tracking-tight">Hackatime</span>
</a>
<nav
class="hidden md:flex gap-8 items-center text-sm font-medium text-secondary"
>
<a
href="/#philosophy"
class="hover:text-surface-content transition-colors">Philosophy</a
>
<a
href="/#features"
class="hover:text-surface-content transition-colors">Features</a
>
<a
href="https://github.com/hackclub/hackatime"
target="_blank"
class="hover:text-surface-content transition-colors">GitHub</a
>
<Link
href="/signin"
class="px-4 py-2 bg-primary text-on-primary rounded-md font-semibold hover:opacity-90 transition-colors"
>
Start tracking
</Link>
</nav>
</div>
</header>
<!-- Hero Section -->
<section class="pt-32 pb-16">
<div class="max-w-[900px] mx-auto px-6">
<h1
class="text-4xl md:text-5xl font-bold tracking-tight leading-[1.15] mb-6"
>
WakaTime Alternative: Free & Open Source Coding Time Tracker
</h1>
<p class="text-lg md:text-xl text-secondary leading-relaxed max-w-[75ch]">
Looking for a WakaTime alternative? <strong class="text-surface-content"
>Hackatime</strong
>
is a free, open source coding time tracker that gives you all the features
you need, without the paywall. Built by
<a
href="https://hackclub.com"
class="text-primary hover:underline"
target="_blank">Hack Club</a
>, it's designed for developers who want powerful analytics without
monthly fees.
</p>
</div>
</section>
<!-- Why Look Section -->
<section class="pb-16">
<div class="max-w-[900px] mx-auto px-6">
<h2 class="text-2xl md:text-3xl font-semibold mb-5">
Why look for a WakaTime alternative?
</h2>
<p class="text-secondary leading-relaxed mb-4 max-w-[75ch]">
WakaTime pioneered automatic coding time tracking, and it's a great
product - in fact, we even use the same editor extensions! At the same
time though, its free tier is pretty limited. You get basic stats, but
advanced features like detailed project breakdowns and longer data
retention require a paid subscription starting at $9/month ($14/month
for dashboard history longer than two weeks). For students, hobbyists,
and open source contributors, that adds up.
</p>
<p class="text-secondary leading-relaxed max-w-[75ch]">
That's where Hackatime comes in. It's built from the ground up to be
<strong class="text-surface-content">completely free</strong> - no freemium
model, no premium tiers, no feature gates.
</p>
</div>
</section>
<!-- Feature Comparison -->
<section class="pb-16">
<div class="max-w-[900px] mx-auto px-6">
<h2 class="text-2xl md:text-3xl font-semibold mb-6">
So, how does Hackatime stack up?
</h2>
<div class="overflow-x-auto rounded-lg border border-surface-200/60">
<table class="w-full border-collapse">
<thead>
<tr class="bg-surface/50">
<th
class="text-left py-3.5 px-5 text-surface-content font-semibold text-sm"
>Feature</th
>
<th
class="text-left py-3.5 px-5 text-primary font-semibold text-sm"
>Hackatime</th
>
<th
class="text-left py-3.5 px-5 text-secondary font-semibold text-sm"
>WakaTime Free</th
>
<th
class="text-left py-3.5 px-5 text-secondary font-semibold text-sm"
>WakaTime Pro</th
>
</tr>
</thead>
<tbody>
{#each features as feature, i}
<tr
class="border-t border-surface-200/40 {i % 2 === 0
? 'bg-surface/20'
: ''}"
>
<td class="py-3 px-5 text-sm text-surface-content font-medium"
>{feature.name}</td
>
<td
class="py-3 px-5 text-sm {feature.hackatimeHighlight
? 'text-primary font-medium'
: 'text-secondary'}">{feature.hackatime}</td
>
<td class="py-3 px-5 text-sm text-secondary"
>{feature.wakatimeFree}</td
>
<td class="py-3 px-5 text-sm text-secondary"
>{feature.wakatimePro}</td
>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</section>
<!-- Honest Trade-offs -->
<section class="pb-16">
<div class="max-w-[900px] mx-auto px-6">
<h2 class="text-2xl md:text-3xl font-semibold mb-5">Honest trade-offs</h2>
<p class="text-secondary leading-relaxed mb-6">
We believe in transparency. Here's what you should know before
switching:
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{#each tradeOffs as tradeOff}
<div
class="p-5 rounded-lg border border-surface-200/40 bg-surface/20"
>
<h3 class="text-surface-content font-semibold mb-2">
{tradeOff.title}
</h3>
<p class="text-secondary text-sm leading-relaxed">
{tradeOff.description}
</p>
</div>
{/each}
</div>
</div>
</section>
<!-- Why Hackatime -->
<section class="pb-16">
<div class="max-w-[900px] mx-auto px-6">
<h2 class="text-2xl md:text-3xl font-semibold mb-5">
Why Hackatime might be right for you
</h2>
<p class="text-secondary leading-relaxed mb-6">
If you're a student, open source contributor, or developer who wants
powerful coding analytics without paying a subscription, Hackatime
delivers:
</p>
<ul class="space-y-3">
{#each benefits as benefit}
<li class="flex items-start gap-3">
<span class="text-primary mt-0.5 flex-shrink-0">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</span>
<span class="text-secondary leading-relaxed">{benefit}</span>
</li>
{/each}
</ul>
</div>
</section>
<!-- Getting Started -->
<section class="pb-16">
<div class="max-w-[900px] mx-auto px-6">
<h2 class="text-2xl md:text-3xl font-semibold mb-5">Getting started</h2>
<p class="text-secondary leading-relaxed mb-6">
Switching from WakaTime takes less than 5 minutes. Since Hackatime uses
the same plugin ecosystem, you just update your <code
class="bg-surface px-2 py-1 rounded text-surface-content text-sm"
>~/.wakatime.cfg</code
> file:
</p>
<pre
class="bg-surface border border-surface-200/40 p-5 rounded-lg overflow-x-auto text-sm text-surface-content mb-6 leading-relaxed"><code
>[settings]
api_url = https://hackatime.hackclub.com/api/hackatime/v1
api_key = YOUR_API_KEY_HERE</code
></pre>
<p class="text-secondary leading-relaxed">
Or use our <Link
href="/my/wakatime_setup"
class="text-primary hover:underline">automated setup page</Link
> that handles everything for you (sign in first!)
</p>
</div>
</section>
<!-- CTA -->
<section class="pb-20">
<div class="max-w-[900px] mx-auto px-6">
<div
class="rounded-xl border border-surface-200/60 bg-surface/30 p-8 md:p-12 text-center"
>
<h2 class="text-2xl md:text-3xl font-bold mb-4">Ready to switch?</h2>
<p class="text-secondary mb-8 max-w-[50ch] mx-auto">
Join thousands of developers who track their coding time for free. No
credit card required.
</p>
<Link
href="/signin"
class="inline-block px-8 py-3.5 bg-primary text-on-primary rounded-md font-semibold text-base hover:opacity-90 transition-colors"
>
Start tracking for free
</Link>
<p class="text-secondary text-sm mt-4">Takes 2 minutes to set up.</p>
</div>
</div>
</section>
<MarketingFooter />
</div>

View file

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
</head>
<body>
<h1>Welcome back to Hackatime!</h1>
<p>Click the link below to sign in to your account:</p>
<p>
<%= link_to 'Sign in to Hackatime', auth_token_url(@token.token) %>
</p>
<p>This link will expire in 30 minutes and can only be used once.</p>
<p>If you didn't request this email, you can safely ignore it.</p>
</body>
</html>

View file

@ -1,18 +0,0 @@
<%- submit_btn_css ||= 'default' %>
<%= form_tag oauth_application_path(application), method: :delete, class: submit_btn_css == 'danger' ? 'w-full' : 'inline' do %>
<% if submit_btn_css == 'danger' %>
<button type="submit" onclick="return confirm(<%= t('doorkeeper.applications.confirmations.destroy').to_json %>)" class="w-full inline-flex items-center justify-center gap-2 px-4 py-2 bg-red hover:opacity-90 text-on-primary font-medium rounded transition-colors duration-200">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<%= t('doorkeeper.applications.buttons.destroy') %>
</button>
<% else %>
<button type="submit" onclick="return confirm(<%= t('doorkeeper.applications.confirmations.destroy').to_json %>)" class="inline-flex items-center gap-1.5 px-3 py-2 bg-red hover:opacity-90 text-on-primary text-sm font-medium rounded transition-colors duration-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete
</button>
<% end %>
<% end %>

View file

@ -1,111 +0,0 @@
<%= form_for application, url: doorkeeper_submit_path(application), as: :doorkeeper_application, html: { class: 'space-y-6' } do |f| %>
<% if application.errors.any? %>
<div class="p-4 bg-red/10 border border-red/20 rounded-lg">
<div class="flex items-center gap-2 text-red mb-2">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
<span class="font-medium"><%= t('doorkeeper.applications.form.error') %></span>
</div>
<ul class="list-disc list-inside text-red/80 text-sm">
<% application.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="border border-primary rounded-xl p-6 bg-dark">
<div class="flex items-center gap-3 mb-6">
<div class="p-2 bg-primary/10 rounded">
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h2 class="text-xl font-semibold text-surface-content">Application Details</h2>
</div>
<div class="space-y-5">
<div>
<%= f.label :name, class: "block text-sm font-medium text-surface-content mb-2" %>
<% if application.persisted? && application.verified? %>
<%= f.text_field :name, class: "w-full px-3 py-2 bg-darkless/50 border border-darkless rounded text-secondary cursor-not-allowed", disabled: true %>
<%= f.hidden_field :name %>
<p class="mt-2 text-xs text-yellow flex items-center gap-1.5">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
</svg>
Name is locked for verified applications. Contact a super admin to change it.
</p>
<% else %>
<%= f.text_field :name, class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-surface-content focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary", placeholder: "My Awesome App", required: true %>
<% end %>
<% if application.errors[:name].present? %>
<p class="mt-1 text-xs text-red"><%= application.errors[:name].to_sentence %></p>
<% end %>
</div>
<div>
<%= f.label :redirect_uri, "Redirect URIs", class: "block text-sm font-medium text-surface-content mb-2" %>
<%= f.text_area :redirect_uri, class: "w-full px-3 py-2 bg-darkless border border-darkless rounded text-surface-content focus:border-primary focus:ring-1 focus:ring-primary placeholder-secondary font-mono text-sm", placeholder: "https://example.com/auth/callback", rows: 3 %>
<% if application.errors[:redirect_uri].present? %>
<p class="mt-1 text-xs text-red"><%= application.errors[:redirect_uri].to_sentence %></p>
<% end %>
<p class="mt-2 text-xs text-secondary">
<%= t('doorkeeper.applications.help.redirect_uri') %>
</p>
<% if Doorkeeper.configuration.allow_blank_redirect_uri?(application) %>
<p class="mt-1 text-xs text-secondary">
<%= t('doorkeeper.applications.help.blank_redirect_uri') %>
</p>
<% end %>
</div>
<div>
<label class="block text-sm font-medium text-surface-content mb-2">Scopes</label>
<% selected_scopes = application.scopes.to_s.split %>
<input type="hidden" name="doorkeeper_application[scopes]" id="doorkeeper_application_scopes" value="<%= selected_scopes.join(' ') %>">
<div class="space-y-3">
<% (Doorkeeper.configuration.default_scopes.to_a + Doorkeeper.configuration.optional_scopes.to_a).uniq.each do |scope| %>
<div class="flex items-start gap-3 p-4 bg-darkless border border-darkless rounded">
<input type="checkbox"
id="scope_<%= scope %>"
value="<%= scope %>"
class="mt-0.5 w-4 h-4 text-primary border-darkless rounded focus:ring-primary bg-darker"
<%= 'checked' if selected_scopes.include?(scope.to_s) || (!application.persisted? && Doorkeeper.configuration.default_scopes.include?(scope)) %>
data-scope-checkbox
onchange="var checks = document.querySelectorAll('[data-scope-checkbox]:checked'); var vals = Array.from(checks).map(function(c){return c.value}); document.getElementById('doorkeeper_application_scopes').value = vals.join(' ');">
<div>
<label for="scope_<%= scope %>" class="text-sm font-medium text-surface-content cursor-pointer">
<%= scope %>
<% if Doorkeeper.configuration.default_scopes.include?(scope) %>
<span class="ml-1.5 text-xs font-normal text-primary">(default)</span>
<% end %>
</label>
<p class="mt-1 text-xs text-secondary"><%= t(scope, scope: [:doorkeeper, :scopes]) %></p>
</div>
</div>
<% end %>
</div>
<% if application.errors[:scopes].present? %>
<p class="mt-1 text-xs text-red"><%= application.errors[:scopes].to_sentence %></p>
<% end %>
</div>
<div class="flex items-start gap-3 p-4 bg-darkless border border-darkless rounded">
<%= f.check_box :confidential, class: "mt-0.5 w-4 h-4 text-primary border-darkless rounded focus:ring-primary bg-darker" %>
<div>
<%= f.label :confidential, "Confidential Application", class: "text-sm font-medium text-surface-content" %>
<p class="mt-1 text-xs text-secondary">
<%= t('doorkeeper.applications.help.confidential') %>
</p>
</div>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<%= f.submit t('doorkeeper.applications.buttons.submit'), class: "px-6 py-2 bg-primary text-on-primary font-medium rounded transition-colors duration-200 hover:opacity-90 cursor-pointer" %>
<%= link_to t('doorkeeper.applications.buttons.cancel'), oauth_applications_path, class: "px-6 py-2 border border-darkless text-surface-content font-medium rounded transition-colors duration-200 hover:bg-darkless" %>
</div>
<% end %>

View file

@ -1,12 +0,0 @@
<% content_for :title do %>
Edit <%= @application.name %>
<% end %>
<div class="max-w-3xl mx-auto p-6 space-y-6">
<header class="text-center mb-8">
<h1 class="text-4xl font-bold text-surface-content mb-2"><%= t('.title') %></h1>
<p class="text-secondary text-lg">Update the settings for <%= @application.name %></p>
</header>
<%= render 'form', application: @application %>
</div>

View file

@ -1,117 +0,0 @@
<% content_for :title do %>
OAuth Applications
<% end %>
<div class="max-w-6xl mx-auto p-6 space-y-6">
<header class="text-center mb-8">
<h1 class="text-4xl font-bold text-surface-content mb-2"><%= t('.title') %></h1>
<p class="text-secondary text-lg">Manage your OAuth applications that integrate with Hackatime</p>
</header>
<div class="flex justify-end mb-6">
<%= link_to new_oauth_application_path,
class: "inline-flex items-center gap-2 px-4 py-2 bg-primary text-on-primary font-medium rounded transition-colors duration-200 hover:opacity-90" do %>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
<%= t('.new') %>
<% end %>
</div>
<% if @applications.any? %>
<div class="grid grid-cols-1 gap-4">
<% @applications.each do |application| %>
<div id="application_<%= application.id %>" class="border border-primary rounded-xl p-6 bg-dark">
<div class="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-3">
<div class="p-2 bg-primary/10 rounded">
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div class="flex items-center gap-2">
<%= link_to application.name, oauth_application_path(application), class: "text-xl font-semibold text-surface-content hover:text-primary transition-colors" %>
<% if application.verified? %>
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-green/20 text-green border border-green/30 rounded text-xs">
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Verified
</span>
<% end %>
</div>
</div>
<div class="space-y-2">
<div class="flex items-start gap-2">
<span class="text-secondary text-sm shrink-0">Callback URLs:</span>
<div class="text-surface-content/80 text-sm break-all">
<% application.redirect_uri.to_s.split.each_with_index do |uri, index| %>
<span class="inline-block bg-darkless px-2 py-0.5 rounded text-xs mb-1"><%= uri %></span>
<% end %>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-secondary text-sm">Confidential:</span>
<% if application.confidential? %>
<span class="inline-flex items-center gap-1 text-green text-sm">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
Yes
</span>
<% else %>
<span class="inline-flex items-center gap-1 text-yellow text-sm">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
No
</span>
<% end %>
</div>
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<%= link_to oauth_application_path(application),
class: "inline-flex items-center gap-1.5 px-3 py-2 bg-darkless hover:bg-secondary/20 text-surface-content text-sm font-medium rounded transition-colors duration-200" do %>
<svg class="w-4 h-4" 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>
View
<% end %>
<%= link_to edit_oauth_application_path(application),
class: "inline-flex items-center gap-1.5 px-3 py-2 bg-primary hover:opacity-90 text-on-primary text-sm font-medium rounded transition-colors duration-200" do %>
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit
<% end %>
<%= render 'delete_form', application: application %>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="border border-primary rounded-xl p-12 bg-dark text-center">
<div class="p-4 bg-primary/10 rounded-full inline-block mb-4">
<svg class="w-12 h-12 text-primary" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 class="text-xl font-semibold text-surface-content mb-2">No applications yet</h3>
<p class="text-secondary mb-6">Create your first OAuth application to start integrating with Hackatime.</p>
<%= link_to new_oauth_application_path,
class: "inline-flex items-center gap-2 px-4 py-2 bg-primary text-on-primary font-medium rounded transition-colors duration-200 hover:opacity-90" do %>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
Create Application
<% end %>
</div>
<% end %>
</div>

View file

@ -1,12 +0,0 @@
<% content_for :title do %>
New OAuth Application
<% end %>
<div class="max-w-3xl mx-auto p-6 space-y-6">
<header class="text-center mb-8">
<h1 class="text-4xl font-bold text-surface-content mb-2"><%= t('.title') %></h1>
<p class="text-secondary text-lg">Create a new OAuth application to integrate with Hackatime</p>
</header>
<%= render 'form', application: @application %>
</div>

View file

@ -1,211 +0,0 @@
<% content_for :title do %>
<%= @application.name %> - OAuth Application
<% end %>
<div class="max-w-4xl mx-auto p-6 space-y-6">
<header class="text-center mb-8">
<h1 class="text-4xl font-bold text-surface-content mb-2"><%= t('.title', name: @application.name) %></h1>
<p class="text-secondary text-lg">OAuth application credentials and settings</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 space-y-6">
<div class="border border-primary rounded-xl p-6 bg-dark">
<div class="flex items-center gap-3 mb-6">
<div class="p-2 bg-primary/10 rounded">
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</div>
<h2 class="text-xl font-semibold text-surface-content">Application Credentials</h2>
</div>
<div class="space-y-5">
<div>
<label class="block text-sm font-medium text-secondary mb-2"><%= t('.application_id') %></label>
<div class="flex items-center gap-2">
<code id="application_id" class="flex-1 px-3 py-2 bg-darkless border border-darkless rounded text-surface-content font-mono text-sm break-all"><%= @application.uid %></code>
<button type="button" onclick="c(this, <%= @application.uid.to_json %>)" class="px-3 py-2 bg-darkless hover:bg-secondary/20 text-surface-content rounded transition-colors" title="Copy">
<svg class="w-5 h-5 ci" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg class="w-5 h-5 c hidden text-green" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium text-secondary mb-2"><%= t('.secret') %></label>
<% secret = flash[:application_secret].presence || @application.plaintext_secret %>
<% if secret.blank? && Doorkeeper.config.application_secret_hashed? %>
<div class="px-3 py-2 bg-darkless border border-darkless rounded">
<span class="text-secondary italic text-sm"><%= t('.secret_hashed') %></span>
</div>
<p class="mt-2 text-xs text-yellow">
<svg class="w-4 h-4 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
The secret was only shown once when the application was created.
</p>
<% else %>
<div class="flex items-center gap-2">
<code id="secret" class="flex-1 px-3 py-2 bg-darkless border border-darkless rounded text-surface-content font-mono text-sm break-all"><%= secret %></code>
<button type="button" onclick="c(this, <%= secret.to_json %>)" class="px-3 py-2 bg-darkless hover:bg-secondary/20 text-surface-content rounded transition-colors" title="Copy">
<svg class="w-5 h-5 ci" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg class="w-5 h-5 c hidden text-green" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</button>
</div>
<% if flash[:application_secret].present? %>
<p class="mt-2 text-xs text-green">
<svg class="w-4 h-4 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
Here are your details. Keep them safe and secure!
</p>
<% end %>
<% end %>
</div>
<div>
<label class="block text-sm font-medium text-secondary mb-2"><%= t('.scopes') %></label>
<% if @application.scopes.present? %>
<div class="flex flex-wrap gap-2">
<% @application.scopes.to_a.each do |scope| %>
<span class="px-2 py-1 bg-primary/20 text-primary border border-primary/30 rounded text-sm font-mono"><%= scope %></span>
<% end %>
</div>
<% else %>
<span class="text-secondary italic text-sm"><%= t('.not_defined') %></span>
<% end %>
</div>
<div>
<label class="block text-sm font-medium text-secondary mb-2"><%= t('.confidential') %></label>
<% if @application.confidential? %>
<span class="inline-flex items-center gap-1.5 px-2 py-1 bg-green/20 text-green border border-green/30 rounded text-sm">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
Yes - Confidential
</span>
<% else %>
<span class="inline-flex items-center gap-1.5 px-2 py-1 bg-yellow/20 text-yellow border border-yellow/30 rounded text-sm">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
No - Public Client
</span>
<% end %>
</div>
<div>
<label class="block text-sm font-medium text-secondary mb-2">Verified Status</label>
<% if @application.verified? %>
<span class="inline-flex items-center gap-1.5 px-2 py-1 bg-green/20 text-green border border-green/30 rounded text-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Verified
</span>
<% else %>
<span class="inline-flex items-center gap-1.5 px-2 py-1 bg-yellow/20 text-yellow border border-yellow/30 rounded text-sm">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
Unverified
</span>
<% end %>
</div>
</div>
</div>
<div class="border border-primary rounded-xl p-6 bg-dark">
<div class="flex items-center gap-3 mb-6">
<div class="p-2 bg-primary/10 rounded">
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
</div>
<h2 class="text-xl font-semibold text-surface-content"><%= t('.callback_urls') %></h2>
</div>
<% if @application.redirect_uri.present? %>
<div class="space-y-3">
<% @application.redirect_uri.split.each do |uri| %>
<div class="flex items-center gap-3 p-3 bg-darkless border border-darkless rounded">
<code class="flex-1 text-surface-content font-mono text-sm break-all"><%= uri %></code>
<%= link_to oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code', scope: @application.scopes),
class: "shrink-0 px-3 py-1.5 bg-green hover:opacity-90 text-on-primary text-sm font-medium rounded transition-colors",
target: '_blank' do %>
Test Auth
<% end %>
</div>
<% end %>
</div>
<% else %>
<p class="text-secondary italic"><%= t('.not_defined') %></p>
<% end %>
</div>
</div>
<div class="space-y-4">
<div class="border border-primary rounded-xl p-6 bg-dark">
<h3 class="text-lg font-semibold text-surface-content mb-4"><%= t('.actions') %></h3>
<div class="space-y-3">
<%= link_to edit_oauth_application_path(@application),
class: "w-full inline-flex items-center justify-center gap-2 px-4 py-2 bg-primary hover:opacity-90 text-on-primary font-medium rounded transition-colors duration-200" do %>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<%= t('doorkeeper.applications.buttons.edit') %>
<% end %>
<%= render 'delete_form', application: @application, submit_btn_css: 'danger' %>
<% if current_user&.admin_level_superadmin? %>
<%= button_to toggle_verified_admin_oauth_application_path(@application),
method: :post,
class: "w-full inline-flex items-center justify-center gap-2 px-4 py-2 #{@application.verified? ? 'bg-yellow hover:bg-yellow/80' : 'bg-green hover:bg-green/80'} text-on-primary font-medium rounded transition-colors duration-200" do %>
<% if @application.verified? %>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
Remove Verification
<% else %>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Verify Application
<% end %>
<% end %>
<% end %>
<%= button_to rotate_secret_oauth_application_path(@application),
method: :post,
data: { turbo_confirm: "Are you sure? This will invalidate your current secrets and break existing integrations" },
class: "w-full inline-flex items-center justify-center gap-2 px-4 py-2 border border-yellow/40 text-yellow hover:bg-yellow/10 font-medium rounded transition-colors duration-200" do %>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Rotate Secret
<% end %>
<%= link_to oauth_applications_path,
class: "w-full inline-flex items-center justify-center gap-2 px-4 py-2 border border-darkless text-surface-content font-medium rounded transition-colors duration-200 hover:bg-darkless" do %>
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Applications
<% end %>
</div>
</div>
</div>
</div>
</div>
<%= render "shared/clipboard_script" %>

View file

@ -1,14 +0,0 @@
<header class="text-center mb-8">
<div class="p-4 bg-red/10 rounded-full inline-block mb-4">
<svg class="w-12 h-12 text-red" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h1 class="font-bold text-3xl text-surface-content"><%= t('doorkeeper.authorizations.error.title') %></h1>
</header>
<div class="bg-red/10 border border-red/20 rounded-lg p-6">
<p class="text-red/80 text-center">
<%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %>
</p>
</div>

View file

@ -1,69 +0,0 @@
<header class="text-center mb-8">
<h1 class="font-bold text-3xl text-surface-content"><%= t('.title') %></h1>
</header>
<% unless @pre_auth.client.application.verified? %>
<div class="bg-yellow/20 border border-yellow rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<svg class="w-6 h-6 text-yellow shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
<div>
<p class="text-yellow font-medium">Unverified Application</p>
<p class="text-yellow text-sm mt-1">This app has not been verified by HQ and could be malicious. Only authorize if you trust the developer.</p>
</div>
</div>
</div>
<% end %>
<div class="bg-dark rounded-lg p-6 mb-8 border border-darkless">
<p class="text-lg text-surface-content mb-4">
<%= sanitize t('.prompt', client_name: content_tag(:strong, class: 'text-primary') { @pre_auth.client.name }), tags: %w[strong], attributes: %w[class] %>
</p>
<% if @pre_auth.scopes.count > 0 %>
<div id="oauth-permissions">
<p class="text-sm text-surface-content mb-3"><%= t('.able_to') %>:</p>
<ul class="space-y-2">
<% @pre_auth.scopes.each do |scope| %>
<li class="flex items-center text-surface-content">
<span class="inline-block w-2 h-2 bg-primary rounded-full mr-3"></span>
<%= t scope, scope: %i[doorkeeper scopes] %>
</li>
<% end %>
</ul>
</div>
<% end %>
</div>
<div class="space-y-3">
<%= form_tag oauth_authorization_path, method: :post, class: "w-full", data: { turbo: false }, onsubmit: "let s=this.querySelector('.spinner'),t=this.querySelector('.btn-text');s.classList.remove('hidden');t.classList.add('hidden');this.querySelector('button').style.cssText='pointer-events:none;opacity:0.7'" do %>
<%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %>
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %>
<%= hidden_field_tag :state, @pre_auth.state, id: nil %>
<%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %>
<%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %>
<%= hidden_field_tag :scope, @pre_auth.scope, id: nil %>
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
<button type="submit" class="w-full px-4 py-3 bg-primary hover:bg-primary/75 text-on-primary font-bold rounded transition-colors cursor-pointer flex items-center justify-center">
<span class="spinner hidden mr-2"><%= render "shared/spinner", class: "h-5 w-5" %></span>
<span class="btn-text"><%= t('doorkeeper.authorizations.buttons.authorize') %></span>
</button>
<% end %>
<%= form_tag oauth_authorization_path, method: :delete, class: "w-full", data: { turbo: false }, onsubmit: "let s=this.querySelector('.spinner'),t=this.querySelector('.btn-text');s.classList.remove('hidden');t.classList.add('hidden');this.querySelector('button').style.cssText='pointer-events:none;opacity:0.7'" do %>
<%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %>
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %>
<%= hidden_field_tag :state, @pre_auth.state, id: nil %>
<%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %>
<%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %>
<%= hidden_field_tag :scope, @pre_auth.scope, id: nil %>
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
<button type="submit" class="w-full px-4 py-3 bg-dark hover:bg-darkless border border-darkless text-muted rounded transition-colors cursor-pointer flex items-center justify-center">
<span class="spinner hidden mr-2"><%= render 'shared/spinner', class: 'h-5 w-5' %></span>
<span class="btn-text"><%= t('doorkeeper.authorizations.buttons.deny') %></span>
</button>
<% end %>
</div>

View file

@ -1,21 +0,0 @@
<header class="text-center mb-8">
<div class="p-4 bg-green/10 rounded-full inline-block mb-4">
<svg class="w-12 h-12 text-green" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h1 class="font-bold text-3xl text-surface-content"><%= t('.title') %></h1>
</header>
<div class="bg-dark rounded-lg p-6 border border-darkless">
<label class="block text-sm font-medium text-secondary mb-2">Authorization Code</label>
<div class="flex items-center gap-2">
<code id="authorization_code" class="flex-1 px-3 py-2 bg-darkless border border-darkless rounded text-surface-content font-mono text-sm break-all"><%= params[:code] %></code>
<button type="button" onclick="navigator.clipboard.writeText(<%= params[:code].to_json %>)" class="px-3 py-2 bg-darkless hover:bg-secondary/20 text-surface-content rounded transition-colors" title="Copy">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
<p class="mt-3 text-xs text-secondary">Copy this code and paste it into your application to complete the authorization.</p>
</div>

View file

@ -221,7 +221,7 @@
<% end %>
<!-- 250px is defined in nav.css -->
<main class="flex-1 <%= 'lg:ml-[250px] lg:max-w-[calc(100%-250px)]' unless content_for?(:hide_nav) %> p-5 mb-[100px] pt-16 lg:pt-5 transition-all duration-300 ease-in-out">
<main class="flex-1 p-5 mb-[100px] pt-16 lg:pt-5 <%= 'lg:ml-[250px] lg:max-w-[calc(100%-250px)]' unless content_for?(:hide_nav) %> transition-all duration-300 ease-in-out">
<%= yield %>
<footer class="relative w-full mt-12 mb-5 p-2.5 text-center text-xs text-muted hover:text-muted transition-colors duration-200">
<div class="container">

View file

@ -1,42 +0,0 @@
<!DOCTYPE html>
<html class="<%= Rails.env == 'production' ? 'production' : 'development' %>" data-theme="<%= current_theme %>">
<head>
<title><%= t('doorkeeper.layouts.admin.title') %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="<%= current_theme_color_scheme %>">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= favicon_link_tag asset_path('favicon.png'), type: 'image/png' %>
<%= stylesheet_link_tag :app %>
<%= javascript_importmap_tags %>
<%= stylesheet_link_tag 'tailwind', 'data-turbo-track': 'reload' %>
</head>
<body class="flex min-h-screen bg-surface" data-controller="nav">
<button class="mobile-nav-button" data-action="click->nav#toggle" data-nav-target="button" aria-label="Toggle navigation menu" aria-expanded="false">
<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" data-nav-target="overlay" data-action="click->nav#close"></div>
<%= render 'shared/nav' %>
<main class="flex-1 lg:ml-[250px] lg:max-w-[calc(100%-250px)] p-5 mb-[100px] pt-16 lg:pt-5 transition-all duration-300 ease-in-out">
<%- if flash[:notice].present? %>
<div class="flash-message flash-message--enter flash-message--success mb-6" data-controller="flash">
<%= flash[:notice] %>
</div>
<% end -%>
<%- if flash[:alert].present? %>
<div class="flash-message flash-message--enter flash-message--error mb-6" data-controller="flash">
<%= flash[:alert] %>
</div>
<% end -%>
<%= yield %>
</main>
</body>
</html>

View file

@ -1,25 +0,0 @@
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center gap-4">
<h1 class="text-3xl font-bold text-surface-content">My Projects</h1>
<% archived_count = current_user.project_repo_mappings.archived.count %>
<% if archived_count > 0 %>
<div class="project-toggle-group">
<%= link_to 'Active', my_projects_path(show_archived: false), class: "project-toggle-btn #{params[:show_archived] != 'true' ? 'active' : 'inactive'}" %>
<%= link_to 'Archived', my_projects_path(show_archived: true), class: "project-toggle-btn #{params[:show_archived] == 'true' ? 'active' : 'inactive'}" %>
</div>
<% end %>
</div>
</div>
<% if current_user.github_uid.blank? %>
<div class="text-red mb-4">
Heads up! You can't link your projects to GitHub until you connect your GitHub account.
<%= link_to 'Sign in with GitHub', github_auth_path, class: 'btn btn-primary text-on-primary underline' %>
</div>
<% end %>
<%= render 'shared/interval_selector' %>
<%= turbo_frame_tag "project_durations", src: project_durations_static_pages_path(interval: params[:interval], from: params[:from], to: params[:to], show_archived: params[:show_archived]), target: "_top" do %>
<%= render 'static_pages/project_durations_skeleton', count: @project_count %>
<% end %>

View file

@ -1,124 +0,0 @@
<%= turbo_frame_tag "interval_selector" do %>
<div class="relative inline-block w-full max-w-xs">
<button type="button" id="interval-dropdown-trigger" class="w-full px-4 py-2 bg-darkless text-muted border border-surface-200 rounded cursor-pointer text-left flex items-center justify-between shadow-lg" onclick="toggleIntervalDropdown()">
<span id="interval-dropdown-label">
<%= human_interval_name(params[:interval], from: params[:from], to: params[:to]) %>
</span>
<svg id="interval-dropdown-arrow" viewBox="0 0 20 20" fill="currentColor" data-slot="icon" aria-hidden="true" class="w-5 h-5 text-muted transition-transform duration-200">
<path d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
</svg>
</button>
<div id="interval-dropdown-menu" class="w-full px-3 py-2 mt-2 bg-darkless text-muted border border-surface-200 rounded-lg absolute z-1000 shadow-lg" style="display: none;">
<ul class="list-none m-0 p-0 overflow-y-auto max-h-60">
<% TimeRangeFilterable::RANGES.each do |key, config| %>
<li class="py-1.5 px-2 cursor-pointer rounded bg-transparent hover:bg-[#2c313a] transition" onclick="selectInterval(<%= key.to_json %>, <%= config[:human_name].to_json %>)"><%= config[:human_name] %></li>
<% end %>
<li class="py-1.5 px-2 cursor-pointer rounded bg-transparent hover:bg-[#2c313a] transition" onclick="selectInterval('', 'All Time')">All Time</li>
</ul>
<hr class="my-2 border-surface-200">
<div class="flex flex-col gap-2 mt-2">
<label class="flex items-center justify-between">Start:
<input type="date" class="ml-2 py-1 px-2 bg-dark border border-surface-200 rounded" id="custom-start" value="<%= params[:from] %>">
</label>
<label class="flex items-center justify-between">End:
<input type="date" class="ml-2 py-1 px-2 bg-dark border border-surface-200 rounded" id="custom-end" value="<%= params[:to] %>">
</label>
<button type="button" class="interval-selector-button px-3 py-2 mt-2 mb-1 rounded font-semibold transition cursor-pointer" onclick="applyCustomRange()">Apply</button>
</div>
</div>
</div>
<script>
// Common function to update URL parameters and refresh the frame
function updateWithParams(params) {
const url = new URL(window.location.href);
// Clear existing parameters
url.searchParams.delete("interval");
url.searchParams.delete("from");
url.searchParams.delete("to");
// Add new parameters
Object.entries(params).forEach(([key, value]) => {
if (value) url.searchParams.set(key, value);
});
updateProjectsFrame(url.toString());
}
function updateProjectsFrame(url) {
let baseUrl = <%== project_durations_static_pages_path.to_json %>;
const params = new URLSearchParams(new URL(url).search);
const frameUrl = baseUrl + "?" + params.toString();
const frame = document.getElementById("project_durations");
if (frame) {
frame.src = frameUrl;
history.replaceState({}, "", url);
} else {
window.location.href = url;
}
}
function updateDropdownLabel(label) {
const labelElement = document.getElementById("interval-dropdown-label");
if (labelElement) {
labelElement.textContent = label;
}
}
function selectInterval(interval, label) {
updateDropdownLabel(label);
document.getElementById("interval-dropdown-menu").style.display = "none";
updateWithParams({ interval });
}
function applyCustomRange() {
const start = document.getElementById("custom-start").value;
const end = document.getElementById("custom-end").value;
if (start || end) {
let label = "Custom Range";
if (start && end) {
label = `${start} to ${end}`;
} else if (start) {
label = `From ${start}`;
} else if (end) {
label = `Until ${end}`;
}
updateDropdownLabel(label);
document.getElementById("interval-dropdown-menu").style.display = "none";
updateWithParams({ interval: "custom", from: start, to: end });
}
}
function toggleIntervalDropdown() {
const menu = document.getElementById("interval-dropdown-menu");
const arrow = document.getElementById("interval-dropdown-arrow");
const isOpen = menu.style.display === "block";
menu.style.display = isOpen ? "none" : "block";
arrow.style.transform = isOpen ? "rotate(0deg)" : "rotate(180deg)";
document.addEventListener("mousedown", closeDropdownOnClickOutside);
}
function closeDropdownOnClickOutside(e) {
const menu = document.getElementById("interval-dropdown-menu");
const trigger = document.getElementById("interval-dropdown-trigger");
const arrow = document.getElementById("interval-dropdown-arrow");
if (!menu.contains(e.target) && !trigger.contains(e.target)) {
menu.style.display = "none";
arrow.style.transform = "rotate(0deg)";
document.removeEventListener("mousedown", closeDropdownOnClickOutside);
}
}
</script>
<% end %>

View file

@ -1,4 +0,0 @@
<svg class="<%= local_assigns[:class] || 'h-6 w-6' %> 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>

Before

Width:  |  Height:  |  Size: 380 B

View file

@ -1,33 +0,0 @@
<% count = [local_assigns[:count] || 6, 1].max %>
<%= turbo_frame_tag "project_durations" do %>
<div class="animate-pulse">
<div class="h-7 w-80 bg-darkless rounded mt-6 mb-6"></div>
<div class="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-6">
<% count.times do %>
<div class="bg-dark border border-primary rounded-xl p-6 shadow-lg flex flex-col gap-4">
<div class="flex justify-between items-start gap-3">
<div class="flex flex-col gap-4 flex-1 min-w-0">
<div class="h-7 w-32 bg-darkless rounded"></div>
<div class="h-6 w-12 bg-darkless rounded"></div>
</div>
<div class="flex gap-2">
<div class="w-8 h-8 bg-darkless rounded-lg"></div>
<div class="w-8 h-8 bg-darkless rounded-lg"></div>
</div>
</div>
<div class="h-8 w-24 bg-darkless rounded"></div>
<div class="h-4 w-full bg-darkless rounded"></div>
<div class="h-4 w-3/4 bg-darkless rounded"></div>
<div class="flex items-center gap-1">
<div class="w-4 h-4 bg-darkless rounded"></div>
<div class="h-4 w-28 bg-darkless rounded"></div>
</div>
</div>
<% end %>
</div>
</div>
<% end %>

View file

@ -302,6 +302,7 @@ Rails.application.routes.draw do
# SEO routes
get "/sitemap.xml", to: "sitemap#sitemap", defaults: { format: "xml" }
get "/wakatime-alternative", to: "static_pages#wakatime_alternative"
# fuck ups
match "/400", to: "errors#bad_request", via: :all