Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/controllers/static_pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ def index
end
end

def hardware
if current_user
redirect_to root_path
else
set_homepage_seo_content
@home_stats = Cache::HomeStatsJob.perform_now
render inertia: "HardwareHome/SignedOut", props: signed_out_props
end
end

def signin
return redirect_to root_path if current_user

Expand Down
218 changes: 218 additions & 0 deletions app/javascript/pages/HardwareHome/SignedOut.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
<script module lang="ts">
import MarketingLayout from "../../layouts/MarketingLayout.svelte";
export const layout = MarketingLayout;
</script>

<script lang="ts">
import { Link } from "@inertiajs/svelte";
import PhilosophySection from "./signedOut/PhilosophySection.svelte";
import FeaturesGrid from "./signedOut/FeaturesGrid.svelte";
import EditorGrid from "./signedOut/EditorGrid.svelte";
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 };
type FlashMessage = { message: string; class_name: string };

let {
home_stats,
flash = [],
}: {
sign_in_email: boolean;
show_dev_tool: boolean;
dev_magic_link?: string | null;
csrf_token: string;
home_stats: HomeStats;
flash?: FlashMessage[];
} = $props();

const fmt = new Intl.NumberFormat("en-US");
const formatNumber = (v: number) => fmt.format(v);
const hoursTracked = $derived(
home_stats?.seconds_tracked
? Math.floor(home_stats.seconds_tracked / 3600)
: 0,
);
const usersTracked = $derived(home_stats?.users_tracked ?? 0);

let flashVisible = $state(false);
let flashHiding = $state(false);

$effect(() => {
if (!flash.length) {
flashVisible = false;
flashHiding = false;
return;
}
flashVisible = true;
flashHiding = false;
let removeId: ReturnType<typeof setTimeout> | undefined;
const hideId = setTimeout(() => {
flashHiding = true;
removeId = setTimeout(() => {
flashVisible = false;
flashHiding = false;
}, 250);
}, 6000);
return () => {
clearTimeout(hideId);
if (removeId) clearTimeout(removeId);
};
});

const NAV_LINKS = [
{ href: "#philosophy", label: "Philosophy" },
{ href: "#features", label: "Features" },
{ href: "#integrations", label: "Integrations" },
{ href: "#faq", label: "FAQ" },
{
href: "https://github.com/hackclub/hackatime",
label: "GitHub",
external: true,
},
];

const STATS = $derived([
{ value: usersTracked, label: "users" },
{ value: hoursTracked, label: "hours tracked" },
]);
</script>

<svelte:head>
<title>Hackatime - Track your development time</title>
</svelte:head>

<div class="landing-page min-h-screen w-full bg-darker text-surface-content">
{#if flashVisible && flash.length > 0}
<div
class="fixed top-4 left-1/2 transform -translate-x-1/2 z-[60] w-full max-w-md px-4 space-y-2"
>
{#each flash as item}
<div
class="flash-message shadow-lg flash-message--enter {flashHiding
? 'flash-message--leaving'
: ''} {item.class_name}"
>
{item.message}
</div>
{/each}
</div>
{/if}

<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"
>
{#each NAV_LINKS as { href, label, external }}
<a
{href}
target={external ? "_blank" : undefined}
class="hover:text-surface-content transition-colors">{label}</a
>
{/each}
<Link
href="/signin"
class="px-4 py-2 bg-primary text-on-primary rounded-md font-semibold hover:opacity-90 transition-colors"
>
Sign in
</Link>
</nav>
</div>
</header>

<section class="pt-40 pb-20">
<div class="max-w-[900px] mx-auto px-6 text-center">
<h1
class="text-5xl md:text-6xl font-bold tracking-tight leading-[1.1] mb-6 text-pretty"
>
The open-source project time tracker
</h1>
<p
class="text-lg md:text-xl text-secondary max-w-[70ch] mx-auto leading-relaxed mb-8"
>
Hackatime is a free, open-source replacement for WakaTime. Your design
habits, project breakdowns and language stats belong to you - not a
proprietary database!
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-10">
<Link
href="/signin"
class="px-7 py-3.5 bg-primary text-on-primary rounded-md font-semibold text-base hover:opacity-90 transition-colors"
>
Start tracking
</Link>
<Link
href="/docs"
class="px-7 py-3.5 bg-surface border border-surface-200 text-surface-content rounded-md font-semibold text-base hover:border-primary hover:text-primary transition-colors"
>
Read the docs
</Link>
</div>

{#if hoursTracked > 0 || usersTracked > 0}
<div
class="flex items-center justify-center gap-8 mb-16 text-secondary text-sm"
>
{#each STATS as { value, label }}
{#if value > 0}
<div class="flex flex-col items-center">
<span class="text-2xl font-bold text-surface-content"
>{formatNumber(value)}</span
>
<span>{label}</span>
</div>
{/if}
{/each}
</div>
{:else}
<div class="mb-16"></div>
{/if}

<div
class="bg-surface border border-surface-200 rounded-lg shadow-lg overflow-hidden"
>
<div
class="bg-surface-100 px-4 py-3 border-b border-surface-200 flex gap-2"
>
<div class="w-2.5 h-2.5 rounded-full bg-primary"></div>
<div class="w-2.5 h-2.5 rounded-full bg-orange"></div>
<div class="w-2.5 h-2.5 rounded-full bg-cyan"></div>
</div>
<div class="bg-surface">
<img
src="/images/hardware-home.webp"
alt="Hackatime Dashboard"
class="w-full h-auto block rounded"
/>
</div>
</div>
</div>
</section>

<PhilosophySection />
<FeaturesGrid />
<EditorGrid />
<HowItWorks />
<FAQSection />
<CTASection
hoursTracked={formatNumber(hoursTracked)}
usersTracked={formatNumber(usersTracked)}
/>

<MarketingFooter />
</div>
124 changes: 124 additions & 0 deletions app/javascript/pages/HardwareHome/signedOut/AuthForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<script lang="ts">
import Button from "../../../components/Button.svelte";
import HackClubLogo from "../../../components/HackClubLogo.svelte";
import { sessions } from "../../../api";
import Slack from "hcicons-svelte/slack";

let {
sign_in_email,
show_dev_tool,
dev_magic_link,
csrf_token,
redirect_to,
continue_param,
}: {
sign_in_email: boolean;
show_dev_tool: boolean;
dev_magic_link?: string | null;
csrf_token: string;
redirect_to?: string;
continue_param?: string | null;
} = $props();

const query = $derived(
continue_param ? { query: { continue: continue_param } } : undefined,
);
const hcaAuthPath = $derived(
query ? sessions.hcaNew.path(query) : sessions.hcaNew.path(),
);
const slackAuthPath = $derived(
query ? sessions.slackNew.path(query) : sessions.slackNew.path(),
);
const emailAuthPath = sessions.email.path();

let isSigningIn = $state(false);
</script>

<div class="w-full max-w-md space-y-4">
{#if sign_in_email}
<div
class="rounded-2xl border border-surface-200 bg-surface p-8 text-center space-y-2"
>
<p class="text-surface-content font-medium">Check your email!</p>
<p class="text-secondary text-sm">
We sent a sign-in link to your inbox. Check your spam if you can't see
it!
</p>
{#if show_dev_tool && dev_magic_link}
<a
href={dev_magic_link}
class="text-xs text-secondary underline hover:text-surface-content"
>
Dev: Open Link
</a>
{/if}
</div>
{:else}
<a
href={hcaAuthPath}
onclick={() => (isSigningIn = true)}
class="w-full flex items-center justify-center gap-3 px-6 py-3.5 rounded-xl bg-primary text-on-primary font-medium hover:opacity-90 transition-all"
>
{#if isSigningIn}
<svg class="h-5 w-5 animate-spin" viewBox="0 0 24 24" fill="none">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{:else}
<HackClubLogo class="h-5 w-5" />
{/if}
<span>Sign in with Hack Club</span>
</a>

<a
href={slackAuthPath}
class="w-full flex items-center justify-center gap-3 px-6 py-3.5 rounded-xl bg-surface border border-surface-200 text-surface-content font-medium hover:bg-surface-100 transition-all"
>
<Slack size={20} />
<span>Sign in with Slack</span>
</a>

<div class="flex items-center gap-4 py-1">
<div class="flex-1 h-px bg-surface-200"></div>
<span class="text-xs text-muted uppercase tracking-wider">or</span>
<div class="flex-1 h-px bg-surface-200"></div>
</div>

<form method="post" action={emailAuthPath} data-turbo="false">
<input type="hidden" name="authenticity_token" value={csrf_token} />
{#if redirect_to}
<input type="hidden" name="redirect_to" value={redirect_to} />
{/if}
{#if continue_param}
<input type="hidden" name="continue" value={continue_param} />
{/if}
<div class="flex gap-2">
<input
type="email"
name="email"
placeholder="you@email.com"
required
class="flex-1 bg-surface text-surface-content placeholder-muted rounded-xl py-3.5 px-4 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all border border-surface-200 focus:border-primary text-sm"
/>
<Button
type="submit"
unstyled
class="px-5 py-3.5 bg-surface border border-primary text-primary rounded-xl hover:bg-primary hover:text-on-primary transition-all text-sm font-medium"
>
Send link
</Button>
</div>
</form>
{/if}
</div>
32 changes: 32 additions & 0 deletions app/javascript/pages/HardwareHome/signedOut/CTASection.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import { Link } from "@inertiajs/svelte";
import Section from "./Section.svelte";

let {
hoursTracked,
usersTracked,
}: { hoursTracked: string; usersTracked: string } = $props();
</script>

<Section bg="bg-primary" py="py-24">
<div class="text-center">
<h2
class="text-3xl md:text-4xl font-semibold text-on-primary tracking-tight mb-4"
>
Start tracking your designing.
</h2>
<p class="text-on-primary/90 text-lg mb-10">
Join {usersTracked} users who have tracked {hoursTracked}+ hours of
development with Hackatime.
</p>
<Link
href="/signin"
class="inline-flex items-center justify-center px-10 py-3.5 bg-surface text-primary rounded-md font-semibold text-lg hover:bg-surface-100 transition-colors"
>
Create free account
</Link>
<div class="mt-6 text-sm text-on-primary/80">
Free to use, forever · Open source (MIT)
</div>
</div>
</Section>
Loading