add new flag to disable some future features and make some updates to chat ux
This commit is contained in:
@@ -39,6 +39,19 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Close the panel with Escape while it's open.
|
||||||
|
$effect(() => {
|
||||||
|
if (!chatStore.open) return;
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
});
|
||||||
|
|
||||||
async function send() {
|
async function send() {
|
||||||
const text = draft.trim();
|
const text = draft.trim();
|
||||||
if (!text || sending) return;
|
if (!text || sending) return;
|
||||||
@@ -82,18 +95,27 @@
|
|||||||
draft = (draft ? draft + ' ' : '') + '@' + name + ' ';
|
draft = (draft ? draft + ' ' : '') + '@' + name + ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render content, wrapping @mentions in a styled token.
|
// Render content, wrapping @mentions in a styled token and https:// URLs in
|
||||||
|
// clickable links (opened in a new window).
|
||||||
function renderContent(text: string) {
|
function renderContent(text: string) {
|
||||||
const segs: { text: string; mention: boolean }[] = [];
|
const segs: { text: string; mention: boolean; url: string | null }[] = [];
|
||||||
const re = /(@[A-Za-z0-9_.-]+)/g;
|
const re = /(@[A-Za-z0-9_.-]+|https?:\/\/[^\s]+)/g;
|
||||||
let last = 0;
|
let last = 0;
|
||||||
let m: RegExpExecArray | null;
|
let m: RegExpExecArray | null;
|
||||||
while ((m = re.exec(text)) !== null) {
|
while ((m = re.exec(text)) !== null) {
|
||||||
if (m.index > last) segs.push({ text: text.slice(last, m.index), mention: false });
|
if (m.index > last) segs.push({ text: text.slice(last, m.index), mention: false, url: null });
|
||||||
segs.push({ text: m[1], mention: true });
|
const tok = m[1];
|
||||||
|
if (tok.startsWith('@')) {
|
||||||
|
segs.push({ text: tok, mention: true, url: null });
|
||||||
|
} else {
|
||||||
|
const url = tok.replace(/[.,;:!?]+$/, '');
|
||||||
|
if (url) segs.push({ text: url, mention: false, url });
|
||||||
|
const rest = tok.slice(url.length);
|
||||||
|
if (rest) segs.push({ text: rest, mention: false, url: null });
|
||||||
|
}
|
||||||
last = m.index + m[0].length;
|
last = m.index + m[0].length;
|
||||||
}
|
}
|
||||||
if (last < text.length) segs.push({ text: text.slice(last), mention: false });
|
if (last < text.length) segs.push({ text: text.slice(last), mention: false, url: null });
|
||||||
return segs;
|
return segs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +154,8 @@
|
|||||||
<button class="mention" onclick={() => insertMention(seg.text.slice(1))}
|
<button class="mention" onclick={() => insertMention(seg.text.slice(1))}
|
||||||
>{seg.text}</button
|
>{seg.text}</button
|
||||||
>
|
>
|
||||||
|
{:else if seg.url}
|
||||||
|
<a class="msg-link" href={seg.url} target="_blank" rel="noopener noreferrer">{seg.url}</a>
|
||||||
{:else}
|
{:else}
|
||||||
{seg.text}
|
{seg.text}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -206,7 +230,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.85rem 1.25rem;
|
height: 52px;
|
||||||
|
padding: 0 1.25rem;
|
||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
background: #4338ca;
|
background: #4338ca;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -309,6 +334,11 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.msg-link {
|
||||||
|
color: #6366f1;
|
||||||
|
text-decoration: underline;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
.chat-typing {
|
.chat-typing {
|
||||||
min-height: 1.5rem;
|
min-height: 1.5rem;
|
||||||
padding: 0 1.25rem;
|
padding: 0 1.25rem;
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ async function seedPlatform(): Promise<void> {
|
|||||||
const created = await fetch(`${PB_ENDPOINT}/api/collections/platform/records`, {
|
const created = await fetch(`${PB_ENDPOINT}/api/collections/platform/records`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${t}` },
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${t}` },
|
||||||
body: JSON.stringify({ label: 'global', flags: { debug: false } })
|
body: JSON.stringify({ label: 'global', flags: { debug: false, rewardsTargets: false } })
|
||||||
});
|
});
|
||||||
const c = await created.json();
|
const c = await created.json();
|
||||||
if (!created.ok) throw new Error(`Seed platform failed: ${JSON.stringify(c)}`);
|
if (!created.ok) throw new Error(`Seed platform failed: ${JSON.stringify(c)}`);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import Footer from '$lib/components/Footer.svelte';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
import FamDone from '$lib/components/FamDone.svelte';
|
import FamDone from '$lib/components/FamDone.svelte';
|
||||||
import { homeIcon, checkCircleIcon, chevronRightIcon } from '$lib/components/icons';
|
import { homeIcon, checkCircleIcon, chevronRightIcon } from '$lib/components/icons';
|
||||||
@@ -8,7 +7,7 @@
|
|||||||
|
|
||||||
let shortcut = $state<(Shortcut & { target: string }) | null>(null);
|
let shortcut = $state<(Shortcut & { target: string }) | null>(null);
|
||||||
|
|
||||||
onMount(() => {
|
$effect(() => {
|
||||||
const s = readShortcut();
|
const s = readShortcut();
|
||||||
if (s) shortcut = { ...s, target: shortcutTarget(s) };
|
if (s) shortcut = { ...s, target: shortcutTarget(s) };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,7 +56,12 @@
|
|||||||
// Kanban columns can collapse (accordion) so templates can be dragged onto
|
// Kanban columns can collapse (accordion) so templates can be dragged onto
|
||||||
// the desired area on tablet/mobile without cramming three columns.
|
// the desired area on tablet/mobile without cramming three columns.
|
||||||
let collapsed = $state({ member: false, comp: false, collab: false });
|
let collapsed = $state({ member: false, comp: false, collab: false });
|
||||||
|
// `rewardsTargets` platform flag: OFF = simplified flow — rewards are always
|
||||||
|
// Individual, and the Competition/Collaboration columns become locked
|
||||||
|
// (faded, "Coming soon") row accordions instead of a 3-column grid.
|
||||||
|
let targetsEnabled = $derived(!!page.data.platformFlags?.rewardsTargets);
|
||||||
function toggleCol(key: 'member' | 'comp' | 'collab') {
|
function toggleCol(key: 'member' | 'comp' | 'collab') {
|
||||||
|
if (!targetsEnabled && key !== 'member') return;
|
||||||
collapsed[key] = !collapsed[key];
|
collapsed[key] = !collapsed[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +151,7 @@
|
|||||||
editVals.targetChoreIds = { ...editVals.targetChoreIds, [memberId]: value };
|
editVals.targetChoreIds = { ...editVals.targetChoreIds, [memberId]: value };
|
||||||
}
|
}
|
||||||
|
|
||||||
let templateConfigs = $derived((templates as BonusTemplate[]).filter((t) => t.global));
|
let templateConfigs = $derived((templates as BonusTemplate[]).filter((t) => t.global && !t.isPocketMoney));
|
||||||
let allBonusConfigs = $derived(
|
let allBonusConfigs = $derived(
|
||||||
configs.filter((c: BonusConfig) => c.status !== 'completed') as BonusConfig[]
|
configs.filter((c: BonusConfig) => c.status !== 'completed') as BonusConfig[]
|
||||||
);
|
);
|
||||||
@@ -212,7 +217,7 @@
|
|||||||
editVals = {
|
editVals = {
|
||||||
name: tpl.name,
|
name: tpl.name,
|
||||||
description: tpl.description || '',
|
description: tpl.description || '',
|
||||||
target: targetType,
|
target: !targetsEnabled ? 'individual' : targetType,
|
||||||
memberId: '',
|
memberId: '',
|
||||||
type: tpl.type,
|
type: tpl.type,
|
||||||
thresholdType: tpl.thresholdType || 'points',
|
thresholdType: tpl.thresholdType || 'points',
|
||||||
@@ -272,7 +277,7 @@
|
|||||||
editVals = {
|
editVals = {
|
||||||
name: cfg.name,
|
name: cfg.name,
|
||||||
description: cfg.description || '',
|
description: cfg.description || '',
|
||||||
target: cfg.target,
|
target: !targetsEnabled ? 'individual' : cfg.target,
|
||||||
memberId: cfg.memberId || '',
|
memberId: cfg.memberId || '',
|
||||||
type: cfg.type,
|
type: cfg.type,
|
||||||
thresholdType: cfg.thresholdType || 'points',
|
thresholdType: cfg.thresholdType || 'points',
|
||||||
@@ -359,7 +364,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card cols={3} scrollX>
|
<Card cols={3} scrollX>
|
||||||
<div class="kanban">
|
<div class="kanban" class:rows={!targetsEnabled}>
|
||||||
<!-- Member Column -->
|
<!-- Member Column -->
|
||||||
<div class="column member-col" class:collapsed={collapsed.member} ondragover={handleDragOver} ondrop={handleDropToMember}>
|
<div class="column member-col" class:collapsed={collapsed.member} ondragover={handleDragOver} ondrop={handleDropToMember}>
|
||||||
<button class="col-header" type="button" onclick={() => toggleCol('member')}>
|
<button class="col-header" type="button" onclick={() => toggleCol('member')}>
|
||||||
@@ -440,10 +445,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Competition Column -->
|
<!-- Competition Column -->
|
||||||
<div class="column comp-col" class:collapsed={collapsed.comp} ondragover={handleDragOver} ondrop={handleDropToCompetition}>
|
<div
|
||||||
|
class="column comp-col"
|
||||||
|
class:locked={!targetsEnabled}
|
||||||
|
class:collapsed={targetsEnabled ? collapsed.comp : true}
|
||||||
|
ondragover={targetsEnabled ? handleDragOver : undefined}
|
||||||
|
ondrop={targetsEnabled ? handleDropToCompetition : undefined}
|
||||||
|
>
|
||||||
<button class="col-header" type="button" onclick={() => toggleCol('comp')}>
|
<button class="col-header" type="button" onclick={() => toggleCol('comp')}>
|
||||||
<span class="col-title">Competition</span>
|
<span class="col-title">Competition</span>
|
||||||
<span class="chev" class:open={!collapsed.comp}>{@html chevronRight}</span>
|
{#if !targetsEnabled}
|
||||||
|
<span class="soon-badge">Coming soon</span>
|
||||||
|
{/if}
|
||||||
|
<span class="chev" class:open={targetsEnabled ? !collapsed.comp : false}>{@html chevronRight}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="col-body">
|
<div class="col-body">
|
||||||
{#each allBonusConfigs.filter((c) => c.target === 'competitive' && !doneConfigs.includes(c)) as cfg}
|
{#each allBonusConfigs.filter((c) => c.target === 'competitive' && !doneConfigs.includes(c)) as cfg}
|
||||||
@@ -506,10 +520,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Collaboration Column -->
|
<!-- Collaboration Column -->
|
||||||
<div class="column collab-col" class:collapsed={collapsed.collab} ondragover={handleDragOver} ondrop={handleDropToCollaboration}>
|
<div
|
||||||
|
class="column collab-col"
|
||||||
|
class:locked={!targetsEnabled}
|
||||||
|
class:collapsed={targetsEnabled ? collapsed.collab : true}
|
||||||
|
ondragover={targetsEnabled ? handleDragOver : undefined}
|
||||||
|
ondrop={targetsEnabled ? handleDropToCollaboration : undefined}
|
||||||
|
>
|
||||||
<button class="col-header" type="button" onclick={() => toggleCol('collab')}>
|
<button class="col-header" type="button" onclick={() => toggleCol('collab')}>
|
||||||
<span class="col-title">Collaboration</span>
|
<span class="col-title">Collaboration</span>
|
||||||
<span class="chev" class:open={!collapsed.collab}>{@html chevronRight}</span>
|
{#if !targetsEnabled}
|
||||||
|
<span class="soon-badge">Coming soon</span>
|
||||||
|
{/if}
|
||||||
|
<span class="chev" class:open={targetsEnabled ? !collapsed.collab : false}>{@html chevronRight}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="col-body">
|
<div class="col-body">
|
||||||
{#each allBonusConfigs.filter((c) => c.target === 'collaborative') as cfg}
|
{#each allBonusConfigs.filter((c) => c.target === 'collaborative') as cfg}
|
||||||
@@ -637,14 +660,19 @@
|
|||||||
<input name="description" bind:value={editVals.description} />
|
<input name="description" bind:value={editVals.description} />
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if targetsEnabled}
|
||||||
<label>
|
<label>
|
||||||
<span class="field-label"><UsersRound size={14} /> Target (who)</span>
|
<span class="field-label"><UsersRound size={14} /> Target (who)</span>
|
||||||
<select name="target" bind:value={editVals.target}>
|
<select name="target" bind:value={editVals.target}>
|
||||||
<option value="individual">Individual</option>
|
<option value="individual">Individual</option>
|
||||||
<option value="competitive">Competition</option>
|
<option value="competitive">Competition</option>
|
||||||
<option value="collaborative">Collaborative</option>
|
<option value="collaborative">Collaborative</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
{:else}
|
||||||
|
<input type="hidden" name="target" value="individual" />
|
||||||
|
<p class="hint">Rewards are always created for individual members.</p>
|
||||||
|
{/if}
|
||||||
{#if (editingConfig || creatingFromTemplate) && editVals.target === 'individual'}
|
{#if (editingConfig || creatingFromTemplate) && editVals.target === 'individual'}
|
||||||
<label>
|
<label>
|
||||||
<span class="field-label"><UsersRound size={14} /> Member</span>
|
<span class="field-label"><UsersRound size={14} /> Member</span>
|
||||||
@@ -1100,6 +1128,33 @@
|
|||||||
.column.collapsed .col-body {
|
.column.collapsed .col-body {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.kanban.rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.kanban.rows .column {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.column.locked {
|
||||||
|
opacity: 0.55;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.column.locked .col-header {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.soon-badge {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0.15rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #6b7280;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.card-head {
|
.card-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -188,6 +188,20 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flag-row">
|
||||||
|
<span class="flag-name">rewardsTargets</span>
|
||||||
|
<span class="flag-desc">Multi-target reward rules (Competition / Collaboration)</span>
|
||||||
|
<form method="POST" action="?/togglePlatformFlag" use:enhance>
|
||||||
|
<input type="hidden" name="flag" value="rewardsTargets" />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="toggle-btn"
|
||||||
|
class:active={page.data.platformFlags?.rewardsTargets}
|
||||||
|
>
|
||||||
|
{page.data.platformFlags?.rewardsTargets ? 'ON' : 'OFF'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</CardGrid>
|
</CardGrid>
|
||||||
|
|
||||||
@@ -923,6 +937,11 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
}
|
}
|
||||||
|
.flag-desc {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.toggle-btn {
|
.toggle-btn {
|
||||||
background: #e5e7eb;
|
background: #e5e7eb;
|
||||||
|
|||||||
Reference in New Issue
Block a user