This commit is contained in:
Mahad Kalam 2026-02-10 02:00:12 +00:00
parent 1d1b1fdcfa
commit 198f9be24d
13 changed files with 25 additions and 369 deletions

1
.gitignore vendored
View file

@ -49,4 +49,3 @@ public/vite-dev
public/vite-ssr
.vite
public/vite

View file

@ -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

View file

@ -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|

View file

@ -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

View file

@ -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}

View file

@ -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}

View file

@ -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"
>
&#129400;
</a>
</span>
{/if}
</div>

View file

@ -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' +

View file

@ -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>

View file

@ -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,
},
},
}),
)

View file

@ -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

View file

@ -12,8 +12,5 @@
"autoBuild": true,
"publicOutputDir": "vite-test",
"port": 3037
},
"production": {
"ssrBuildEnabled": true
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB