-
+
@@ -523,13 +524,9 @@ export const DashboardGroupZone = memo(function DashboardGroupZone({
{selectedActiveSession
- ? formatSessionDate(
- selectedActiveSession.started_at ??
- selectedActiveSession.scheduled_at,
- locale,
- )
+ ? getCompactSessionMeta(selectedActiveSession)
: selectedNextSession
- ? formatSessionDate(selectedNextSession.scheduled_at, locale)
+ ? getCompactSessionMeta(selectedNextSession)
: `${selectedGroup.memberCount} ${labels.members}`}
@@ -538,20 +535,23 @@ export const DashboardGroupZone = memo(function DashboardGroupZone({
event.stopPropagation()}
- className="relative inline-flex h-8 max-w-[68px] shrink-0 items-center justify-center rounded-[9px] border border-white/[0.08] px-1.5 text-[11px] font-semibold text-[#e8f4f0]"
+ className="relative inline-flex h-8 max-w-[64px] shrink-0 items-center justify-center rounded-[9px] border border-white/[0.08] px-2 text-[11px] font-semibold text-[#e8f4f0]"
>
{mobileJoinLabel}
+ ) : selectedNextSession && sessionHref ? (
+
event.stopPropagation()}
+ className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#20D9A3] text-[#062b22] shadow-[0_0_22px_rgba(32,217,163,0.22)]"
+ aria-label={labels.openSession}
+ >
+
+
) : null}
-
{isOpen ? (
-
+
{labels.groupsListTitle}
@@ -625,7 +625,7 @@ export const DashboardGroupZone = memo(function DashboardGroupZone({
setIsOverflowOpen((current) => !current);
setIsOpen(false);
}}
- className="flex h-full min-h-[66px] w-full items-center justify-center rounded-[13px] border border-white/[0.06] bg-white/[0.018] text-[#d7e3df] transition hover:border-white/[0.1] hover:bg-white/[0.04]"
+ className="flex h-full min-h-[72px] w-full items-center justify-center rounded-[13px] border border-white/[0.06] bg-white/[0.018] text-[#d7e3df] transition hover:border-white/[0.1] hover:bg-white/[0.04]"
aria-expanded={isOverflowOpen}
aria-label={labels.dropdownLabel}
>
@@ -751,6 +751,38 @@ export const DashboardGroupZone = memo(function DashboardGroupZone({
+
+
+
+
) : null}
@@ -1396,6 +1428,86 @@ export const DashboardGroupZone = memo(function DashboardGroupZone({
);
});
+function MobileSessionList({
+ locale,
+ labels,
+ selectedGroup,
+ selectedActiveSession,
+ selectedNextSession,
+ sessionHref,
+}: {
+ locale: string;
+ labels: DashboardGroupZoneProps['labels'];
+ selectedGroup: DashboardGroupZoneGroup;
+ selectedActiveSession: DashboardGroupZoneSession | null;
+ selectedNextSession: DashboardGroupZoneSession | null;
+ sessionHref: string | null;
+}) {
+ const secondarySessions = selectedGroup.recentSessions?.filter(
+ (session) =>
+ session.id !== selectedActiveSession?.id &&
+ session.id !== selectedNextSession?.id,
+ );
+ const secondarySession =
+ selectedNextSession ?? secondarySessions?.[0] ?? selectedActiveSession;
+ const primarySession = selectedActiveSession ?? selectedNextSession;
+
+ return (
+
+ );
+}
+
function formatSessionDate(value: string, locale: string) {
return new Intl.DateTimeFormat(locale, {
weekday: 'short',
@@ -1406,6 +1518,25 @@ function formatSessionDate(value: string, locale: string) {
}).format(new Date(value));
}
+function formatShortDate(value: string, locale: string) {
+ return new Intl.DateTimeFormat(locale, {
+ month: 'short',
+ day: 'numeric',
+ }).format(new Date(value));
+}
+
+function getCompactSessionMeta(session: DashboardGroupZoneSession) {
+ return `${session.answeredQuestionCount ?? 0}/${session.question_goal} Q · ${session.timer_seconds} sec`;
+}
+
+function getMobileSessionSummary(
+ session: DashboardGroupZoneSession,
+ locale: string,
+) {
+ const scheduledAt = formatShortDate(session.scheduled_at, locale);
+ return `${session.answeredQuestionCount ?? 0} Q · ${scheduledAt} · ${session.timer_seconds} sec`;
+}
+
function getLiveSessionProgress(session: DashboardGroupZoneSession) {
const total = Math.max(
1,
@@ -1756,6 +1887,42 @@ function PanelStat({ label, value }: { label: string; value: string }) {
);
}
+function CompactAvatarStack({
+ members,
+}: {
+ members: DashboardGroupZoneGroup['membersPreview'];
+}) {
+ const safeMembers = members ?? [];
+
+ if (safeMembers.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {safeMembers.map((member, index) => (
+
+ {member.avatarUrl ? null : member.initials}
+
+ ))}
+
+ );
+}
+
function MemberAvatarStack({
members,
}: {
diff --git a/lib/supabase/types.ts b/lib/supabase/types.ts
index b778372..a930983 100644
--- a/lib/supabase/types.ts
+++ b/lib/supabase/types.ts
@@ -205,33 +205,42 @@ type GroupsRow = {
created_at: string;
created_by: string | null;
difficulty_level: 'low' | 'medium' | 'high';
+ group_kind: 'manual' | 'session_test' | 'solidified';
id: string;
invite_code: string;
+ last_session_id: string | null;
max_members: number;
meeting_link: string | null;
name: string;
+ solidified_at: string | null;
};
type GroupsInsert = {
created_at?: string;
created_by?: string | null;
difficulty_level?: 'low' | 'medium' | 'high';
+ group_kind?: 'manual' | 'session_test' | 'solidified';
id?: string;
invite_code: string;
+ last_session_id?: string | null;
max_members?: number;
meeting_link?: string | null;
name: string;
+ solidified_at?: string | null;
};
type GroupsUpdate = {
created_at?: string;
created_by?: string | null;
difficulty_level?: 'low' | 'medium' | 'high';
+ group_kind?: 'manual' | 'session_test' | 'solidified';
id?: string;
invite_code?: string;
+ last_session_id?: string | null;
max_members?: number;
meeting_link?: string | null;
name?: string;
+ solidified_at?: string | null;
};
type GroupInvitesRow = {
@@ -532,7 +541,9 @@ type SessionsRow = {
leader_id: string | null;
meeting_link: string | null;
name: string | null;
+ planned_from_session_id: string | null;
question_goal: number;
+ review_timer_seconds: number;
scheduled_at: string;
share_code: string;
started_at: string | null;
@@ -549,7 +560,9 @@ type SessionsInsert = {
leader_id?: string | null;
meeting_link?: string | null;
name?: string | null;
+ planned_from_session_id?: string | null;
question_goal?: number;
+ review_timer_seconds?: number;
scheduled_at: string;
share_code?: string;
started_at?: string | null;
@@ -566,7 +579,9 @@ type SessionsUpdate = {
leader_id?: string | null;
meeting_link?: string | null;
name?: string | null;
+ planned_from_session_id?: string | null;
question_goal?: number;
+ review_timer_seconds?: number;
scheduled_at?: string;
share_code?: string;
started_at?: string | null;
diff --git a/supabase/migrations/20260622120000_session_first_foundations.sql b/supabase/migrations/20260622120000_session_first_foundations.sql
new file mode 100644
index 0000000..15089db
--- /dev/null
+++ b/supabase/migrations/20260622120000_session_first_foundations.sql
@@ -0,0 +1,51 @@
+alter table public.groups
+ add column if not exists group_kind text not null default 'manual'
+ check (group_kind in ('manual', 'session_test', 'solidified')),
+ add column if not exists solidified_at timestamptz,
+ add column if not exists last_session_id uuid references public.sessions(id) on delete set null;
+
+create index if not exists idx_groups_group_kind
+ on public.groups (group_kind);
+
+alter table public.sessions
+ add column if not exists review_timer_seconds integer
+ check (review_timer_seconds is null or review_timer_seconds between 60 and 86400),
+ add column if not exists planned_from_session_id uuid references public.sessions(id) on delete set null;
+
+update public.sessions
+set review_timer_seconds = least(86400, greatest(60, coalesce(question_goal, 20) * 180))
+where review_timer_seconds is null;
+
+alter table public.sessions
+ alter column review_timer_seconds set default 3600,
+ alter column review_timer_seconds set not null;
+
+create index if not exists idx_sessions_planned_from_session_id
+ on public.sessions (planned_from_session_id)
+ where planned_from_session_id is not null;
+
+create or replace function public.activeboard_set_review_timer_seconds()
+returns trigger
+language plpgsql
+set search_path = public
+as $$
+begin
+ if new.review_timer_seconds is null then
+ new.review_timer_seconds := least(86400, greatest(60, coalesce(new.question_goal, 20) * 180));
+ end if;
+
+ return new;
+end;
+$$;
+
+drop trigger if exists set_review_timer_seconds_before_insert on public.sessions;
+create trigger set_review_timer_seconds_before_insert
+before insert on public.sessions
+for each row
+execute function public.activeboard_set_review_timer_seconds();
+
+update public.app_policy_settings
+set default_question_goal = 20,
+ updated_at = timezone('utc', now())
+where id = 'default'
+ and default_question_goal = 10;