mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 18:45:21 +00:00
Remove unused ERBs, /wakatime-alternative (#976)
* Remove unused ERBs, /wakatime-alternative * Update grid icons * Fix classes
This commit is contained in:
parent
32c97bbf1b
commit
3fbc925572
24 changed files with 526 additions and 933 deletions
|
|
@ -1,3 +1,8 @@
|
||||||
vendor/
|
vendor/
|
||||||
swagger/
|
swagger/
|
||||||
docs/
|
docs/
|
||||||
|
spec/fixtures/heartbeats.json
|
||||||
|
package-lock.json
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.*.json
|
||||||
|
db/migrate/
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
class StaticPagesController < InertiaController
|
class StaticPagesController < InertiaController
|
||||||
include DashboardData
|
include DashboardData
|
||||||
|
|
||||||
layout "inertia", only: :index
|
layout "inertia", only: %i[index wakatime_alternative]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
if current_user
|
if current_user
|
||||||
|
|
@ -114,6 +114,18 @@ class StaticPagesController < InertiaController
|
||||||
render partial: "streak"
|
render partial: "streak"
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def set_homepage_seo_content
|
def set_homepage_seo_content
|
||||||
|
|
|
||||||
96
app/javascript/components/MarketingFooter.svelte
Normal file
96
app/javascript/components/MarketingFooter.svelte
Normal 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>
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
import HowItWorks from "./signedOut/HowItWorks.svelte";
|
import HowItWorks from "./signedOut/HowItWorks.svelte";
|
||||||
import FAQSection from "./signedOut/FAQSection.svelte";
|
import FAQSection from "./signedOut/FAQSection.svelte";
|
||||||
import CTASection from "./signedOut/CTASection.svelte";
|
import CTASection from "./signedOut/CTASection.svelte";
|
||||||
|
import MarketingFooter from "../../components/MarketingFooter.svelte";
|
||||||
|
|
||||||
type HomeStats = { seconds_tracked?: number; users_tracked?: number };
|
type HomeStats = { seconds_tracked?: number; users_tracked?: number };
|
||||||
|
|
||||||
|
|
@ -194,96 +195,5 @@
|
||||||
usersTracked={formatNumber(usersTracked)}
|
usersTracked={formatNumber(usersTracked)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<footer class="py-16 w-full bg-surface">
|
<MarketingFooter />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,37 +4,38 @@
|
||||||
title: "Time tracking",
|
title: "Time tracking",
|
||||||
description:
|
description:
|
||||||
"Automatic tracking that starts when you type and stops when you take a break. Down to the second.",
|
"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",
|
title: "Language stats",
|
||||||
description:
|
description:
|
||||||
"See your actual tech stack. How much Rust vs. how much time debugging config files.",
|
"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",
|
title: "File-level insights",
|
||||||
description:
|
description:
|
||||||
"Drill into specific files. Find the ones consuming all your time.",
|
"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",
|
title: "Zero lock-in",
|
||||||
description:
|
description:
|
||||||
"Your data is yours. Export your entire history to JSON or CSV via the REST API.",
|
"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)",
|
title: "Leaderboards (coming soon)",
|
||||||
description:
|
description:
|
||||||
"Create private leaderboards for your team or hackathon. Compete on consistency.",
|
"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",
|
title: "Privacy-first",
|
||||||
description:
|
description:
|
||||||
"We don't sell your data. We don't use it for AI training. Just heartbeats and charts.",
|
"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>
|
</script>
|
||||||
|
|
@ -47,7 +48,7 @@
|
||||||
>
|
>
|
||||||
Everything you need, nothing you don't.
|
Everything you need, nothing you don't.
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-secondary text-lg">
|
<p class="text-secondary text-lg text-pretty">
|
||||||
Granular telemetry for your development environment, delivered via a
|
Granular telemetry for your development environment, delivered via a
|
||||||
clean dashboard.
|
clean dashboard.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -57,10 +58,8 @@
|
||||||
<div class="p-8 bg-surface border border-surface-200 rounded-lg">
|
<div class="p-8 bg-surface border border-surface-200 rounded-lg">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="2"
|
fill="currentColor"
|
||||||
stroke="currentColor"
|
|
||||||
class="w-8 h-8 text-purple mb-4"
|
class="w-8 h-8 text-purple mb-4"
|
||||||
>
|
>
|
||||||
{@html feature.icon}
|
{@html feature.icon}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-secondary text-base leading-relaxed">
|
<p class="text-secondary text-base leading-relaxed">
|
||||||
Other trackers delete your history after two weeks unless you pay.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-8 bg-darker border border-surface-200 rounded-lg">
|
<div class="p-8 bg-darker border border-surface-200 rounded-lg">
|
||||||
|
|
|
||||||
397
app/javascript/pages/WakatimeAlternative.svelte
Normal file
397
app/javascript/pages/WakatimeAlternative.svelte
Normal 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>
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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 %>
|
|
||||||
|
|
@ -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 %>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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" %>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -221,7 +221,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<!-- 250px is defined in nav.css -->
|
<!-- 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 %>
|
<%= 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">
|
<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">
|
<div class="container">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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 %>
|
|
||||||
|
|
@ -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 %>
|
|
||||||
|
|
@ -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 |
|
|
@ -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 %>
|
|
||||||
|
|
@ -302,6 +302,7 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
# SEO routes
|
# SEO routes
|
||||||
get "/sitemap.xml", to: "sitemap#sitemap", defaults: { format: "xml" }
|
get "/sitemap.xml", to: "sitemap#sitemap", defaults: { format: "xml" }
|
||||||
|
get "/wakatime-alternative", to: "static_pages#wakatime_alternative"
|
||||||
|
|
||||||
# fuck ups
|
# fuck ups
|
||||||
match "/400", to: "errors#bad_request", via: :all
|
match "/400", to: "errors#bad_request", via: :all
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue