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
32 changes: 32 additions & 0 deletions app/controllers/docs_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class DocsController < InertiaController
layout "inertia"

inertia_share docs_nav: -> { docs_nav_props }

POPULAR_EDITORS = [
[ "VS Code", "vs-code" ], [ "PyCharm", "pycharm" ], [ "IntelliJ IDEA", "intellij-idea" ],
[ "Sublime Text", "sublime-text" ], [ "Vim", "vim" ], [ "Neovim", "neovim" ],
Expand Down Expand Up @@ -104,6 +106,36 @@ def show

private

def docs_nav_props
current_path = request.path
{
current_path: current_path,
home_url: docs_path,
api_docs_url: "/api-docs",
github_url: "https://github.com/hackclub/hackatime",
slack_url: "https://hackclub.slack.com/archives/C07MQ845X1F",
sections: [
{
title: "Getting Started",
links: [
{ label: "Quick Start", href: doc_path("getting-started/quick-start") },
{ label: "Installation", href: doc_path("getting-started/installation") },
{ label: "Configuration", href: doc_path("getting-started/configuration") }
]
},
{
title: "Developers",
links: [
{ label: "API Docs", href: "/api-docs/" },
{ label: "OAuth Apps", href: doc_path("oauth/oauth-apps") }
]
}
],
popular_editors: POPULAR_EDITORS,
all_editors: ALL_EDITORS
}
end

def sanitize_path(path)
# Remove any directory traversal attempts and normalize path
return "index" if path.blank?
Expand Down
7 changes: 0 additions & 7 deletions app/controllers/extensions_controller.rb

This file was deleted.

4 changes: 2 additions & 2 deletions app/controllers/inertia_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ def inertia_primary_links
if current_user
links << inertia_link("Projects", my_projects_path, active: request.path.start_with?("/my/projects"), inertia: true)
links << inertia_link("Docs", docs_path, active: helpers.current_page?(docs_path) || request.path.start_with?("/docs"), inertia: true)
links << inertia_link("Extensions", extensions_path, active: helpers.current_page?(extensions_path), inertia: true)
links << inertia_link("Setup", my_wakatime_setup_path, active: helpers.current_page?(my_wakatime_setup_path), inertia: true)
links << inertia_link("Settings", my_settings_path, active: request.path.start_with?("/my/settings"), inertia: true)
links << inertia_link("My OAuth Apps", oauth_applications_path, active: helpers.current_page?(oauth_applications_path) || request.path.start_with?("/oauth/applications"), inertia: true)
links << { label: "Logout", action: "logout" }
else
links << inertia_link("Docs", docs_path, active: helpers.current_page?(docs_path) || request.path.start_with?("/docs"), inertia: true)
links << inertia_link("Extensions", extensions_path, active: helpers.current_page?(extensions_path), inertia: true)
links << inertia_link("Setup", my_wakatime_setup_path, active: helpers.current_page?(my_wakatime_setup_path), inertia: true)
end

links
Expand Down
2 changes: 0 additions & 2 deletions app/helpers/extensions_helper.rb

This file was deleted.

3 changes: 1 addition & 2 deletions app/javascript/layouts/AppLayout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,7 @@
const navLinkWithToolClass = (link: NavLink, toolClass = "") =>
`${navLinkClass(link.active)}${toolClass ? ` ${toolClass}` : ""}`;

const isLongCachedLink = (link: NavLink) =>
link.label === "Docs" || link.label === "Extensions";
const isLongCachedLink = (link: NavLink) => link.label === "Docs";

const linkCacheFor = (link: NavLink): string | [string, string] =>
isLongCachedLink(link) ? "10m" : ["0s", "30s"];
Expand Down
311 changes: 311 additions & 0 deletions app/javascript/layouts/DocsLayout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
<script lang="ts">
import { Link } from "@inertiajs/svelte";
import Button from "../components/Button.svelte";
import type { Snippet } from "svelte";
import { onMount, onDestroy } from "svelte";

type DocsNavLink = { label: string; href: string };
type DocsNavSection = { title: string; links: DocsNavLink[] };
type DocsNav = {
current_path: string;
home_url: string;
api_docs_url: string;
github_url: string;
slack_url: string;
sections: DocsNavSection[];
popular_editors: [string, string][];
all_editors: [string, string][];
};

let { docs_nav, children }: { docs_nav: DocsNav; children?: Snippet } =
$props();

const isBrowser =
typeof window !== "undefined" && typeof document !== "undefined";

let navOpen = $state(false);

const toggleNav = () => (navOpen = !navOpen);
const closeNav = () => (navOpen = false);

const isActive = (href: string) => {
if (!href) return false;
const currentPath = docs_nav.current_path || "/docs";
if (href === docs_nav.home_url) {
return currentPath === docs_nav.home_url || currentPath === "/docs";
}
return currentPath === href || currentPath.startsWith(`${href}/`);
};

const handleNavLinkClick = () => {
if (isBrowser && window.innerWidth <= 1024) closeNav();
};

const handleResize = () => {
if (isBrowser && window.innerWidth > 1024) closeNav();
};

const handleKeydown = (e: KeyboardEvent) => {
if (e.key === "Escape") closeNav();
};

$effect(() => {
if (isBrowser) document.body.classList.toggle("overflow-hidden", navOpen);
});

onMount(() => {
if (!isBrowser) return;
handleResize();
window.addEventListener("resize", handleResize);
document.addEventListener("keydown", handleKeydown);
});

onDestroy(() => {
if (isBrowser) {
window.removeEventListener("resize", handleResize);
document.removeEventListener("keydown", handleKeydown);
}
});

const inactiveNavClass =
"text-surface-content hover:bg-darkless hover:text-primary";
const activeNavClass = "bg-darkless text-primary";
const navItemClass = "rounded-md px-3 py-2 text-sm transition-colors";
const inlineNavItemClass = `flex items-center gap-2 ${navItemClass}`;
const splitNavItemClass = `flex items-center justify-between ${navItemClass}`;

const navLinkClass = (active?: boolean) =>
`block ${navItemClass} ${active ? activeNavClass : inactiveNavClass}`;

const inlineNavLinkClass = (active?: boolean) =>
`${inlineNavItemClass} ${active ? activeNavClass : inactiveNavClass}`;

const sectionTitleClass =
"px-3 pt-3 pb-1 text-[0.6875rem] font-semibold uppercase tracking-wider text-secondary/80";

const resourceLinkClass = `${splitNavItemClass} ${inactiveNavClass}`;

const secondaryNavLinkClass = `block ${navItemClass} text-secondary hover:bg-darkless hover:text-primary`;

const externalIconClass = "size-3 text-muted";
</script>

<Button
type="button"
unstyled
class="mobile-nav-button"
aria-label="Toggle docs navigation"
aria-expanded={navOpen}
onclick={toggleNav}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</Button>

<Button
type="button"
unstyled
class={`nav-overlay ${navOpen ? "open" : ""}`}
onclick={closeNav}
aria-label="Close docs navigation"
></Button>

<aside
class={`fixed inset-y-0 left-0 z-1000 flex min-h-dvh w-60 flex-col overflow-y-auto border-r border-r-darkless bg-dark px-3 py-4 text-surface-content transition-transform duration-300 ease-in-out [scrollbar-width:none] [-ms-overflow-style:none] lg:block ${
navOpen
? "translate-x-0 shadow-[2px_0_20px_rgba(0,0,0,0.3)]"
: "max-lg:-translate-x-full"
}`}
>
<!-- Branding / header -->
<div class="px-3 pb-3 mb-2 border-b border-darkless">
<Link
href={docs_nav.home_url}
onclick={handleNavLinkClick}
class="group flex items-center gap-2"
>
<div class="flex flex-col">
<div class="text-sm font-bold text-surface-content">Hackatime</div>
<div class="text-[0.6875rem] uppercase tracking-wider text-secondary">
Documentation
</div>
</div>
</Link>
</div>

<nav class="space-y-1">
<!-- Overview -->
<Link
href={docs_nav.home_url}
prefetch
cacheFor="10m"
onclick={handleNavLinkClick}
class={navLinkClass(isActive(docs_nav.home_url))}
>
Overview
</Link>

<!-- Sections -->
{#each docs_nav.sections as section}
<div class={sectionTitleClass}>{section.title}</div>
{#each section.links as link}
<Link
href={link.href}
prefetch
cacheFor="10m"
onclick={handleNavLinkClick}
class={navLinkClass(isActive(link.href))}
>
{link.label}
</Link>
{/each}
{/each}

<!-- Editors -->
<div class={sectionTitleClass}>Editors</div>
{#each docs_nav.popular_editors.slice(0, 8) as [name, slug]}
{@const active = isActive(`/docs/editors/${slug}`)}
<Link
href={`/docs/editors/${slug}`}
prefetch
cacheFor="10m"
onclick={handleNavLinkClick}
class={inlineNavLinkClass(active)}
>
<img
src={`/images/editor-icons/${slug}-128.png`}
alt=""
class="size-4 shrink-0"
loading="lazy"
/>
<span class="truncate">{name}</span>
</Link>
{/each}
<Link
href={docs_nav.home_url}
onclick={handleNavLinkClick}
class={secondaryNavLinkClass}
>
All {docs_nav.all_editors.length} editors →
</Link>

<!-- Resources -->
<div class={sectionTitleClass}>Resources</div>
<a
href={docs_nav.api_docs_url}
onclick={handleNavLinkClick}
class={resourceLinkClass}
>
API Reference
<svg
class={externalIconClass}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"
/>
</svg>
</a>
<a
href={docs_nav.github_url}
target="_blank"
rel="noopener"
onclick={handleNavLinkClick}
class={resourceLinkClass}
Comment on lines +227 to +231
>
GitHub
<svg
class={externalIconClass}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
<a
href={docs_nav.slack_url}
target="_blank"
rel="noopener"
onclick={handleNavLinkClick}
class={resourceLinkClass}
>
Comment on lines +250 to +254
#hackatime-help
<svg
class={externalIconClass}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>

<!-- Back to app -->
<div class="pt-3 mt-3 border-t border-darkless">
<a
href="/"
onclick={handleNavLinkClick}
class={`${inlineNavItemClass} text-secondary hover:bg-darkless hover:text-primary`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
Back to Hackatime
</a>
</div>
</nav>
</aside>

<main class="min-h-dvh flex-1 transition-all duration-300 ease-in-out lg:ml-72">
<div class="mx-auto w-full max-w-5xl p-4 pt-16 md:p-8 lg:pt-8">
{@render children?.()}
</div>
</main>

<style>
:global(#app) {
display: flex;
flex: 1 1 auto;
min-height: 100vh;
width: 100%;
}
</style>
Loading
Loading