mirror of
https://github.com/System-End/hackatime.git
synced 2026-04-19 22:15:14 +00:00
AGH
This commit is contained in:
parent
1d1b1fdcfa
commit
198f9be24d
13 changed files with 25 additions and 369 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -49,4 +49,3 @@ public/vite-dev
|
|||
public/vite-ssr
|
||||
|
||||
.vite
|
||||
public/vite
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
web: bin/rails server -b 0.0.0.0
|
||||
css: bin/rails tailwindcss:watch
|
||||
vite: bin/vite dev
|
||||
ssr: bin/vite ssr
|
||||
|
|
|
|||
|
|
@ -18,20 +18,19 @@ class InertiaController < ApplicationController
|
|||
end
|
||||
|
||||
def inertia_nav_props
|
||||
user = current_user
|
||||
{
|
||||
flash: inertia_flash_messages,
|
||||
user_present: user.present?,
|
||||
user: user ? inertia_user_mention_props(user) : nil,
|
||||
streak: user ? inertia_streak_props(user) : nil,
|
||||
admin_level: user&.admin_level == "default" ? nil : user&.admin_level,
|
||||
user_present: current_user.present?,
|
||||
user_mention_html: current_user ? render_to_string(partial: "shared/user_mention", locals: { user: current_user }) : nil,
|
||||
streak_html: current_user ? render_to_string(partial: "static_pages/streak", locals: { user: current_user, show_text: true, turbo_frame: false }) : nil,
|
||||
admin_level_html: current_user ? render_to_string(partial: "static_pages/admin_level", locals: { user: current_user }) : nil,
|
||||
login_path: slack_auth_path,
|
||||
links: inertia_primary_links,
|
||||
dev_links: inertia_dev_links,
|
||||
admin_links: inertia_admin_links,
|
||||
viewer_links: inertia_viewer_links,
|
||||
superadmin_links: inertia_superadmin_links,
|
||||
activities: inertia_activities_props
|
||||
activities_html: inertia_activities_html
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -120,22 +119,9 @@ class InertiaController < ApplicationController
|
|||
{ label: label, href: href, active: active, badge: badge }
|
||||
end
|
||||
|
||||
def inertia_activities_props
|
||||
def inertia_activities_html
|
||||
return nil unless defined?(@activities) && @activities.present?
|
||||
|
||||
@activities.filter_map do |activity|
|
||||
owner = activity.owner
|
||||
next unless owner
|
||||
|
||||
message = inertia_activity_message(activity)
|
||||
next if message.blank?
|
||||
|
||||
{
|
||||
id: activity.id,
|
||||
owner: inertia_user_mention_props(owner),
|
||||
message: message
|
||||
}
|
||||
end
|
||||
helpers.render_activities(@activities)
|
||||
end
|
||||
|
||||
def inertia_footer_props
|
||||
|
|
@ -163,82 +149,6 @@ class InertiaController < ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
def inertia_user_mention_props(user)
|
||||
title =
|
||||
if user == current_user
|
||||
FlavorText.same_user.sample
|
||||
else
|
||||
user.github_username.presence || user.slack_username.presence
|
||||
end
|
||||
country_name = user.country_code.present? ? user.country_name : nil
|
||||
|
||||
{
|
||||
display_name: user.display_name,
|
||||
avatar_url: user.avatar_url,
|
||||
title: title,
|
||||
country_code: user.country_code,
|
||||
country_name: country_name,
|
||||
impersonate_path: impersonate_path_for(user)
|
||||
}
|
||||
end
|
||||
|
||||
def inertia_streak_props(user)
|
||||
streak_display = user.streak_days_formatted
|
||||
return nil if streak_display.blank?
|
||||
|
||||
streak_count = streak_display.to_i
|
||||
{
|
||||
count: streak_count,
|
||||
display: streak_display,
|
||||
title: streak_count > 30 ? "30+ daily streak" : "#{streak_display} day streak",
|
||||
show_text: true,
|
||||
icon_size: 24
|
||||
}
|
||||
end
|
||||
|
||||
def inertia_activity_message(activity)
|
||||
params = activity.parameters&.with_indifferent_access || {}
|
||||
project = params[:project].presence
|
||||
|
||||
case activity.key
|
||||
when "user.first_signup"
|
||||
"just signed in for the first time"
|
||||
when "user.first_heartbeat"
|
||||
base = "just started tracking their coding time"
|
||||
project ? "#{base} on #{project}!" : "#{base}!"
|
||||
when "user.started_working"
|
||||
base = "just started working"
|
||||
project ? "#{base} on #{project}" : base
|
||||
when "user.coding_session"
|
||||
base = "just finished coding"
|
||||
duration = helpers.short_time_simple(params[:duration_seconds].to_i)
|
||||
if project
|
||||
"#{base} on #{project} for #{duration}"
|
||||
else
|
||||
"#{base} for #{duration}"
|
||||
end
|
||||
when "physical_mail.mail_sent"
|
||||
mission_type = params[:humanized_mission_type].presence || "a mission"
|
||||
"was just sent a letter for '#{mission_type}'"
|
||||
when "physical_mail.first_streak_achieved"
|
||||
"just hit their first 7 day coding streak!"
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def impersonate_path_for(user)
|
||||
return nil unless current_user
|
||||
return nil unless current_user.admin_level.in?(%w[admin superadmin])
|
||||
return nil if user == current_user
|
||||
return nil if user.admin_level == "superadmin"
|
||||
|
||||
return impersonate_user_path(user) if user.admin_level != "admin"
|
||||
return impersonate_user_path(user) if current_user.admin_level == "superadmin"
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def currently_hacking_props
|
||||
data = Cache::CurrentlyHackingJob.perform_now
|
||||
users = (data[:users] || []).map do |u|
|
||||
|
|
|
|||
|
|
@ -213,9 +213,9 @@ class StaticPagesController < InertiaController
|
|||
|
||||
Time.use_zone(current_user.timezone) do
|
||||
filters.each do |f|
|
||||
durations = current_user.heartbeats.group(f).duration_seconds
|
||||
durations = durations.reject { |n, _| archived.include?(n) } if f == :project
|
||||
result[f] = durations.sort_by { |_, v| -v }.map(&:first).compact_blank.map { |k|
|
||||
options = current_user.heartbeats.distinct.pluck(f).compact_blank
|
||||
options = options.reject { |n| archived.include?(n) } if f == :project
|
||||
result[f] = options.map { |k|
|
||||
f == :language ? k.categorize_language : (%i[operating_system editor].include?(f) ? k.capitalize : k)
|
||||
}.uniq
|
||||
|
||||
|
|
@ -233,7 +233,6 @@ class StaticPagesController < InertiaController
|
|||
end
|
||||
|
||||
hb = hb.filter_by_time_range(interval, params[:from], params[:to])
|
||||
result[:filtered_heartbeats] = hb
|
||||
result[:total_time] = hb.group(:project).duration_seconds.values.sum
|
||||
result[:total_heartbeats] = hb.count
|
||||
|
||||
|
|
@ -270,13 +269,12 @@ class StaticPagesController < InertiaController
|
|||
}.to_h
|
||||
end
|
||||
|
||||
result[:weekly_project_stats] = (0..25).to_h do |w|
|
||||
result[:weekly_project_stats] = (0..11).to_h do |w|
|
||||
ws = w.weeks.ago.beginning_of_week
|
||||
[ ws.to_date.iso8601, hb.where(time: ws.to_f..w.weeks.ago.end_of_week.to_f)
|
||||
.group(:project).duration_seconds.reject { |p, _| archived.include?(p) } ]
|
||||
end
|
||||
end
|
||||
|
||||
result[:selected_interval] = interval.to_s
|
||||
result[:selected_from] = params[:from].to_s
|
||||
result[:selected_to] = params[:to].to_s
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
<script lang="ts">
|
||||
type AdminLevel = "superadmin" | "admin" | "viewer" | "default";
|
||||
|
||||
let { level }: { level: AdminLevel | null | undefined } = $props();
|
||||
|
||||
const levelConfig: Record<string, { label: string; className: string }> = {
|
||||
superadmin: {
|
||||
label: "Superadmin",
|
||||
className: "text-red-500 font-semibold px-2 superadmin-tool",
|
||||
},
|
||||
admin: {
|
||||
label: "Admin",
|
||||
className: "text-yellow-500 font-semibold px-2 admin-tool",
|
||||
},
|
||||
viewer: {
|
||||
label: "Viewer",
|
||||
className: "text-blue-500 font-semibold px-2 viewer-tool",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if level && levelConfig[level]}
|
||||
<span class={levelConfig[level].className}>{levelConfig[level].label}</span>
|
||||
{/if}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
<script lang="ts">
|
||||
type NavStreak = {
|
||||
count: number;
|
||||
display: string;
|
||||
title: string;
|
||||
show_text?: boolean;
|
||||
icon_size?: number;
|
||||
show_super_class?: boolean;
|
||||
};
|
||||
|
||||
let { streak }: { streak: NavStreak } = $props();
|
||||
|
||||
const streakClasses = (count: number) => {
|
||||
if (count >= 30) {
|
||||
return {
|
||||
bg: "from-blue-900/20 to-indigo-900/20",
|
||||
hbg: "hover:from-blue-800/30 hover:to-indigo-800/30",
|
||||
bc: "border-blue-700",
|
||||
ic: "text-blue-400 group-hover:text-blue-300",
|
||||
tc: "text-blue-300 group-hover:text-blue-200",
|
||||
tm: "text-blue-400",
|
||||
};
|
||||
}
|
||||
if (count >= 7) {
|
||||
return {
|
||||
bg: "from-red-900/20 to-orange-900/20",
|
||||
hbg: "hover:from-red-800/30 hover:to-orange-800/30",
|
||||
bc: "border-red-700",
|
||||
ic: "text-red-400 group-hover:text-red-300",
|
||||
tc: "text-red-300 group-hover:text-red-200",
|
||||
tm: "text-red-400",
|
||||
};
|
||||
}
|
||||
return {
|
||||
bg: "from-orange-900/20 to-yellow-900/20",
|
||||
hbg: "hover:from-orange-800/30 hover:to-yellow-800/30",
|
||||
bc: "border-orange-700",
|
||||
ic: "text-orange-400 group-hover:text-orange-300",
|
||||
tc: "text-orange-300 group-hover:text-orange-200",
|
||||
tm: "text-orange-400",
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if streak?.count > 0}
|
||||
{@const styles = streakClasses(streak.count)}
|
||||
{@const showText = streak.show_text ?? false}
|
||||
{@const iconSize = streak.icon_size ?? 24}
|
||||
{@const superClass = streak.show_super_class ? "super" : ""}
|
||||
<div
|
||||
class={`inline-flex items-center gap-1 px-2 py-1 bg-gradient-to-r ${styles.bg} border ${styles.bc} rounded-lg transition-all duration-200 ${styles.hbg} group ${superClass}`}
|
||||
title={streak.title}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={iconSize}
|
||||
height={iconSize}
|
||||
viewBox="0 0 24 24"
|
||||
class={`${styles.ic} transition-colors duration-200 group-hover:animate-pulse`}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M10 2c0-.88 1.056-1.331 1.692-.722c1.958 1.876 3.096 5.995 1.75 9.12l-.08.174l.012.003c.625.133 1.203-.43 2.303-2.173l.14-.224a1 1 0 0 1 1.582-.153C18.733 9.46 20 12.402 20 14.295C20 18.56 16.409 22 12 22s-8-3.44-8-7.706c0-2.252 1.022-4.716 2.632-6.301l.605-.589c.241-.236.434-.43.618-.624C9.285 5.268 10 3.856 10 2"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span
|
||||
class={`text-md font-semibold ${styles.tc} transition-colors duration-200`}
|
||||
>
|
||||
{streak.display}
|
||||
{#if showText}
|
||||
<span class={`ml-1 font-normal ${styles.tm}`}>day streak</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
<script lang="ts">
|
||||
type NavUserMention = {
|
||||
display_name: string;
|
||||
avatar_url?: string | null;
|
||||
title?: string | null;
|
||||
country_code?: string | null;
|
||||
country_name?: string | null;
|
||||
impersonate_path?: string | null;
|
||||
};
|
||||
|
||||
let { user }: { user: NavUserMention } = $props();
|
||||
const flagUrl = $derived(countryFlagUrl(user.country_code));
|
||||
|
||||
const countryFlagUrl = (countryCode?: string | null) => {
|
||||
if (!countryCode) return null;
|
||||
const upper = countryCode.toUpperCase();
|
||||
if (upper.length !== 2) return null;
|
||||
const codepoints = Array.from(upper).map(
|
||||
(char) => 0x1f1e6 + char.charCodeAt(0) - "A".charCodeAt(0),
|
||||
);
|
||||
const hex = codepoints.map((codepoint) => codepoint.toString(16)).join("-");
|
||||
return `https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/${hex}.svg`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="user-info flex items-center gap-2" title={user.title || undefined}>
|
||||
{#if user.avatar_url}
|
||||
<img
|
||||
src={user.avatar_url}
|
||||
alt={`${user.display_name}'s avatar`}
|
||||
class="rounded-full aspect-square border border-gray-300"
|
||||
width="32"
|
||||
height="32"
|
||||
loading="lazy"
|
||||
/>
|
||||
{/if}
|
||||
<span class="inline-flex items-center gap-1">
|
||||
{user.display_name}
|
||||
</span>
|
||||
{#if flagUrl}
|
||||
<span title={user.country_name || undefined} class="flex items-center">
|
||||
<img
|
||||
src={flagUrl}
|
||||
alt={`${user.country_code} flag`}
|
||||
class="inline-block w-5 h-5 align-middle"
|
||||
loading="lazy"
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
{#if user.impersonate_path}
|
||||
<span class="admin-tool">
|
||||
<a
|
||||
href={user.impersonate_path}
|
||||
class="text-primary font-bold hover:text-red-300 transition-colors duration-200"
|
||||
data-turbo-frame="_top"
|
||||
data-turbo-prefetch="false"
|
||||
>
|
||||
🥸
|
||||
</a>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { createInertiaApp, type ResolvedComponent } from '@inertiajs/svelte'
|
||||
import { hydrate, mount } from 'svelte'
|
||||
import { mount } from 'svelte'
|
||||
import AppLayout from '../layouts/AppLayout.svelte'
|
||||
|
||||
createInertiaApp({
|
||||
|
|
@ -22,9 +22,7 @@ createInertiaApp({
|
|||
|
||||
setup({ el, App, props }) {
|
||||
if (el) {
|
||||
const hasServerMarkup = el.hasChildNodes()
|
||||
const render = hasServerMarkup ? hydrate : mount
|
||||
render(App, { target: el, props })
|
||||
mount(App, { target: el, props })
|
||||
} else {
|
||||
console.error(
|
||||
'Missing root element.\n\n' +
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@
|
|||
import { usePoll } from "@inertiajs/svelte";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import plur from "plur";
|
||||
import NavAdminLevelBadge from "../components/nav/NavAdminLevelBadge.svelte";
|
||||
import NavStreakBadge from "../components/nav/NavStreakBadge.svelte";
|
||||
import NavUserMention from "../components/nav/NavUserMention.svelte";
|
||||
|
||||
type NavLink = {
|
||||
label: string;
|
||||
|
|
@ -15,43 +12,19 @@
|
|||
action?: string;
|
||||
};
|
||||
|
||||
type NavUserMentionType = {
|
||||
display_name: string;
|
||||
avatar_url?: string | null;
|
||||
title?: string | null;
|
||||
country_code?: string | null;
|
||||
country_name?: string | null;
|
||||
impersonate_path?: string | null;
|
||||
};
|
||||
|
||||
type NavStreak = {
|
||||
count: number;
|
||||
display: string;
|
||||
title: string;
|
||||
show_text?: boolean;
|
||||
icon_size?: number;
|
||||
show_super_class?: boolean;
|
||||
};
|
||||
|
||||
type NavActivity = {
|
||||
id: number;
|
||||
owner?: NavUserMentionType | null;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type LayoutNav = {
|
||||
flash: { message: string; class_name: string }[];
|
||||
user_present: boolean;
|
||||
user?: NavUserMentionType | null;
|
||||
streak?: NavStreak | null;
|
||||
admin_level?: string | null;
|
||||
user_mention_html?: string | null;
|
||||
streak_html?: string | null;
|
||||
admin_level_html?: string | null;
|
||||
login_path: string;
|
||||
links: NavLink[];
|
||||
dev_links: NavLink[];
|
||||
admin_links: NavLink[];
|
||||
viewer_links: NavLink[];
|
||||
superadmin_links: NavLink[];
|
||||
activities?: NavActivity[] | null;
|
||||
activities_html?: string | null;
|
||||
};
|
||||
|
||||
type Footer = {
|
||||
|
|
@ -238,15 +211,11 @@
|
|||
<div
|
||||
class="flex flex-col items-center gap-2 pb-3 border-b border-darkless"
|
||||
>
|
||||
{#if layout.nav.user}
|
||||
<NavUserMention user={layout.nav.user} />
|
||||
{/if}
|
||||
{#if layout.nav.streak}
|
||||
<NavStreakBadge streak={layout.nav.streak} />
|
||||
{/if}
|
||||
{#if layout.nav.admin_level}
|
||||
<NavAdminLevelBadge level={layout.nav.admin_level} />
|
||||
{/if}
|
||||
{#if layout.nav.user_mention_html}{@html layout.nav
|
||||
.user_mention_html}{/if}
|
||||
{#if layout.nav.streak_html}{@html layout.nav.streak_html}{/if}
|
||||
{#if layout.nav.admin_level_html}{@html layout.nav
|
||||
.admin_level_html}{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
|
|
@ -347,17 +316,8 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if layout.nav.activities && layout.nav.activities.length > 0}
|
||||
<div class="pt-2 space-y-2">
|
||||
{#each layout.nav.activities as activity (activity.id)}
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
{#if activity.owner}
|
||||
<NavUserMention user={activity.owner} />
|
||||
{/if}
|
||||
<span>{activity.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if layout.nav.activities_html}
|
||||
<div class="pt-2">{@html layout.nav.activities_html}</div>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
import { createInertiaApp, type ResolvedComponent } from '@inertiajs/svelte'
|
||||
import createServer from '@inertiajs/svelte/server'
|
||||
import { render } from 'svelte/server'
|
||||
import AppLayout from '../layouts/AppLayout.svelte'
|
||||
|
||||
createServer((page) =>
|
||||
createInertiaApp({
|
||||
page,
|
||||
|
||||
resolve: (name) => {
|
||||
const pages = import.meta.glob<ResolvedComponent>('../pages/**/*.svelte', {
|
||||
eager: true,
|
||||
})
|
||||
const pageComponent = pages[`../pages/${name}.svelte`]
|
||||
if (!pageComponent) {
|
||||
console.error(`Missing Inertia page component: '${name}.svelte'`)
|
||||
}
|
||||
|
||||
return {
|
||||
default: pageComponent.default,
|
||||
layout: pageComponent.layout || AppLayout,
|
||||
} as ResolvedComponent
|
||||
},
|
||||
|
||||
setup({ App, props }) {
|
||||
return render(App, { props })
|
||||
},
|
||||
|
||||
defaults: {
|
||||
form: {
|
||||
forceIndicesArrayFormatInFormData: false,
|
||||
},
|
||||
future: {
|
||||
useScriptElementForInitialPage: true,
|
||||
useDataInertiaHeadAttribute: true,
|
||||
useDialogForErrorModal: true,
|
||||
preserveEqualProps: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
@ -6,6 +6,4 @@ InertiaRails.configure do |config|
|
|||
config.always_include_errors_hash = true
|
||||
config.use_script_element_for_initial_page = true
|
||||
config.use_data_inertia_head_attribute = true
|
||||
config.ssr_enabled = ViteRuby.config.ssr_build_enabled
|
||||
config.ssr_url = ENV.fetch("INERTIA_SSR_URL", "http://127.0.0.1:13714")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,8 +12,5 @@
|
|||
"autoBuild": true,
|
||||
"publicOutputDir": "vite-test",
|
||||
"port": 3037
|
||||
},
|
||||
"production": {
|
||||
"ssrBuildEnabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
Loading…
Add table
Reference in a new issue