Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import MagnifyingGlass from 'svelte-heros-v2/MagnifyingGlass.svelte';
import Plus from 'svelte-heros-v2/Plus.svelte';
import Users from 'svelte-heros-v2/Users.svelte';
import XMark from 'svelte-heros-v2/XMark.svelte';
import { goto } from '$app/navigation';
import type { CollectiveListParams } from '$lib/api/collectives';
import { createI18n } from '$lib/i18n/index.js';
import { collectives, collectivesList, collectiveTypes } from '$lib/stores/collectives';
Expand Down Expand Up @@ -143,6 +144,13 @@ let sortedItems = $derived.by(() => {
});
return items;
});

// Open the first result when pressing Enter in the search box
function openFirstResult() {
const first = sortedItems[0];
if (!first) return;
goto(`/collectives/${first.id}`);
}
</script>

<div class="space-y-4">
Expand All @@ -154,6 +162,7 @@ let sortedItems = $derived.by(() => {
type="text"
value={searchQuery}
oninput={(e) => handleSearchInput(e.currentTarget.value)}
onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); openFirstResult(); } }}
placeholder={$i18n.t('collectives.searchPlaceholder')}
class="w-full pl-12 pr-12 py-3 text-base font-body text-gray-900 placeholder-gray-400 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-forest focus:border-transparent"
autocomplete="off"
Expand Down
38 changes: 26 additions & 12 deletions apps/frontend/src/lib/components/encounters/encounter-detail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import { goto } from '$app/navigation';
import MarkdownView from '$lib/editor/markdown-view.svelte';
import { createI18n } from '$lib/i18n/index.js';
import { encounters } from '$lib/stores/encounters';
import {
isOpenEncounterFriendModeActive,
openEncounterFriendModePrefix,
visibleEncounterFriendIds,
} from '$lib/stores/ui';
import type { Encounter } from '$shared';
import FriendAvatar from '../friends/friend-avatar.svelte';
import KeyboardHintBadge from '../keyboard-hint-badge.svelte';
import { encounterDisplayTitle, encounterTypeLabel } from './encounter-display';
import EncounterTypeIcon from './encounter-type-icon.svelte';

Expand All @@ -26,6 +32,11 @@ let showDeleteConfirm = $state(false);

let displayTitle = $derived(encounterDisplayTitle($i18n.t, encounter));

// Track visible friend IDs for keyboard open mode (o)
$effect(() => {
visibleEncounterFriendIds.set(encounter.friends.map((f) => f.id));
});

function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString(undefined, {
Expand Down Expand Up @@ -112,18 +123,21 @@ async function handleDelete() {
<span class="text-sm font-body font-normal text-white/80">({encounter.friends.length})</span>
</h2>
<div class="flex flex-wrap gap-3 p-3 bg-gray-50 rounded-lg">
{#each encounter.friends as friend (friend.id)}
<a
href="/friends/{friend.id}"
class="inline-flex items-center gap-2 bg-white border border-gray-200 px-3 py-2 rounded-lg hover:border-forest transition-colors"
>
<FriendAvatar
displayName={friend.displayName}
photoUrl={friend.photoUrl}
size="sm"
/>
<span class="font-body text-sm text-gray-900">{friend.displayName}</span>
</a>
{#each encounter.friends as friend, index (friend.id)}
<div class="relative">
<KeyboardHintBadge {index} isActive={$isOpenEncounterFriendModeActive} prefix={$openEncounterFriendModePrefix} variant="card" />
<a
href="/friends/{friend.id}"
class="inline-flex items-center gap-2 bg-white border border-gray-200 px-3 py-2 rounded-lg hover:border-forest transition-colors"
>
<FriendAvatar
displayName={friend.displayName}
photoUrl={friend.photoUrl}
size="sm"
/>
<span class="font-body text-sm text-gray-900">{friend.displayName}</span>
</a>
</div>
{/each}
</div>
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import ChevronLeft from 'svelte-heros-v2/ChevronLeft.svelte';
import ChevronRight from 'svelte-heros-v2/ChevronRight.svelte';
import MagnifyingGlass from 'svelte-heros-v2/MagnifyingGlass.svelte';
import Plus from 'svelte-heros-v2/Plus.svelte';
import { goto } from '$app/navigation';
import type { EncounterListParams } from '$lib/api/encounters';
import { createI18n } from '$lib/i18n/index.js';
import { encounters, encountersList } from '$lib/stores/encounters';
import { visibleEncounterIds } from '$lib/stores/ui';
import { ENCOUNTER_TYPES, type EncounterType } from '$shared';
import { encounterTypeLabel } from './encounter-display';
import EncounterCard from './encounter-card.svelte';
import { encounterTypeLabel } from './encounter-display';

const i18n = createI18n();

Expand Down Expand Up @@ -42,6 +43,13 @@ $effect(() => {
visibleEncounterIds.set(ids);
});

// Open the first result when pressing Enter in the search box
function openFirstResult() {
const first = encounterItems[0];
if (!first) return;
goto(`/encounters/${first.id}`);
}

// Load encounters on mount (not in $effect to avoid infinite loop)
onMount(() => {
loadEncounters();
Expand Down Expand Up @@ -116,8 +124,10 @@ function goToPage(page: number) {
type="text"
value={searchQuery}
oninput={(e) => handleSearchInput(e.currentTarget.value)}
onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); openFirstResult(); } }}
placeholder={$i18n.t('encounters.searchPlaceholder')}
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-forest focus:border-transparent font-body text-sm"
data-search-input
/>
</div>
</div>
Expand Down
16 changes: 9 additions & 7 deletions apps/frontend/src/lib/components/friends/friend-detail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,17 @@ onMount(() => {
</div>
</div>

<!-- ==================== PROFESSIONAL HISTORY SECTION ==================== -->
<!-- Always mounted so the add-work-experience shortcut/listener is registered
even when the friend has no employment entries yet -->
<ProfessionalHistorySection
friendId={friend.id}
professionalHistory={friend.professionalHistory ?? []}
/>

<!-- ==================== ABOUT SECTION ==================== -->
{#if (friend.professionalHistory && friend.professionalHistory.length > 0) || friend.interests || friend.metInfo}
{#if friend.interests || friend.metInfo}
<div class="space-y-4">
<!-- Professional History -->
<ProfessionalHistorySection
friendId={friend.id}
professionalHistory={friend.professionalHistory ?? []}
/>

<!-- Interests -->
{#if friend.interests}
<section class="space-y-2">
Expand Down
12 changes: 12 additions & 0 deletions apps/frontend/src/lib/components/friends/friend-list.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import MagnifyingGlass from 'svelte-heros-v2/MagnifyingGlass.svelte';
import Plus from 'svelte-heros-v2/Plus.svelte';
import Users from 'svelte-heros-v2/Users.svelte';
import XMark from 'svelte-heros-v2/XMark.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import * as friendsApi from '$lib/api/friends';
import { createI18n } from '$lib/i18n/index.js';
Expand Down Expand Up @@ -390,6 +391,16 @@ let gridItems = $derived.by<FriendGridItem[]>(() => {
return $friendList.map(toFriendGridItem);
}
});

// Open the first result when pressing Enter in the search box
function openFirstResult() {
const first = gridItems[0];
if (!first) return;
const url = returnUrl
? `/friends/${first.id}?from=${encodeURIComponent(returnUrl)}`
: `/friends/${first.id}`;
goto(url);
}
</script>

<div class="space-y-4">
Expand All @@ -401,6 +412,7 @@ let gridItems = $derived.by<FriendGridItem[]>(() => {
type="text"
value={searchQuery}
oninput={(e) => handleSearchInput(e.currentTarget.value)}
onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); openFirstResult(); } }}
placeholder={$i18n.t('friendList.searchPlaceholder')}
class="w-full pl-12 pr-12 py-3 text-base font-body text-gray-900 placeholder-gray-400 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-forest focus:border-transparent"
autocomplete="off"
Expand Down
18 changes: 18 additions & 0 deletions apps/frontend/src/lib/shortcuts/components/help-dialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,24 @@ const i18n = createI18n();
<span class="text-gray-700">{$i18n.t('shortcuts.help.editEncounter')}</span>
<kbd class="px-2 py-1 bg-gray-100 border border-gray-300 rounded text-sm font-mono">e</kbd>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-700">{$i18n.t('shortcuts.help.openItem19', { item: $i18n.t('shortcuts.help.friend') })}</span>
<div class="flex gap-1">
<kbd class="px-2 py-1 bg-gray-100 border border-gray-300 rounded text-sm font-mono">o</kbd>
<span class="text-gray-400">{$i18n.t('shortcuts.help.then')}</span>
<kbd class="px-2 py-1 bg-gray-100 border border-gray-300 rounded text-sm font-mono">1-9</kbd>
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-700">{$i18n.t('shortcuts.help.openItem10', { item: $i18n.t('shortcuts.help.friend') })}</span>
<div class="flex gap-1">
<kbd class="px-2 py-1 bg-gray-100 border border-gray-300 rounded text-sm font-mono">o</kbd>
<span class="text-gray-400">{$i18n.t('shortcuts.help.then')}</span>
<kbd class="px-2 py-1 bg-gray-100 border border-gray-300 rounded text-sm font-mono">a-z</kbd>
<span class="text-gray-400">{$i18n.t('shortcuts.help.then')}</span>
<kbd class="px-2 py-1 bg-gray-100 border border-gray-300 rounded text-sm font-mono">1-9</kbd>
</div>
</div>
</div>
</div>
{/if}
Expand Down
11 changes: 11 additions & 0 deletions apps/frontend/src/lib/shortcuts/config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import {
FILTER_CATEGORY_KEYS,
isOpenCollectiveModeActive,
isOpenEncounterFriendModeActive,
isOpenEncounterModeActive,
isOpenFriendLinkModeActive,
isOpenMemberModeActive,
isOpenModeActive,
openCollectiveModePrefix,
openEncounterFriendModePrefix,
openEncounterModePrefix,
openFriendLinkModePrefix,
openMemberModePrefix,
openModePrefix,
visibleCollectiveIds,
visibleEncounterFriendIds,
visibleEncounterIds,
visibleFriendDetailLinks,
visibleFriendIds,
Expand Down Expand Up @@ -126,6 +129,14 @@ export const OPEN_MODE_CONFIGS: OpenModeConfig[] = [
basePath: '/friends',
modeName: 'member',
},
{
routeMatch: /^\/encounters\/[^/]+$/,
itemIdsStore: visibleEncounterFriendIds,
prefixStore: openEncounterFriendModePrefix,
activeStore: isOpenEncounterFriendModeActive,
basePath: '/friends',
modeName: 'encounterFriend',
},
{
routeMatch: '/friends',
itemIdsStore: visibleFriendIds,
Expand Down
7 changes: 7 additions & 0 deletions apps/frontend/src/lib/shortcuts/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isDeleteCircleModeActive,
isEditCircleModeActive,
isOpenCollectiveModeActive,
isOpenEncounterFriendModeActive,
isOpenEncounterModeActive,
isOpenFriendLinkModeActive,
isOpenMemberModeActive,
Expand Down Expand Up @@ -133,6 +134,12 @@ export function createKeydownHandler(callbacks: KeydownHandlerCallbacks) {
isOpenFriendLinkModeActive.set(true);
return;
}
if (pathname.match(/^\/encounters\/[^/]+$/) && !pathname.endsWith('/new')) {
e.preventDefault();
callbacks.setPendingKey('o');
isOpenEncounterFriendModeActive.set(true);
return;
}
}

// Start filter sequence (only on friends list page)
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/src/lib/shortcuts/keyboard-shortcuts.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import {
isEditCircleModeActive,
isFilterModeActive,
isOpenCollectiveModeActive,
isOpenEncounterFriendModeActive,
isOpenEncounterModeActive,
isOpenFriendLinkModeActive,
isOpenMemberModeActive,
isOpenModeActive,
openCollectiveModePrefix,
openEncounterFriendModePrefix,
openEncounterModePrefix,
openFriendLinkModePrefix,
openMemberModePrefix,
Expand Down Expand Up @@ -67,6 +69,8 @@ function clearPending() {
openMemberModePrefix.set(null);
isOpenFriendLinkModeActive.set(false);
openFriendLinkModePrefix.set(null);
isOpenEncounterFriendModeActive.set(false);
openEncounterFriendModePrefix.set(null);
}

// The handler is recreated when showHelp changes so guards get fresh state
Expand Down Expand Up @@ -120,6 +124,8 @@ const handleKeydown = $derived(
<HintModePanel title={$i18n.t('shortcuts.panels.openCollective')} prefix={$openCollectiveModePrefix} itemDescription={$i18n.t('shortcuts.items.collective')} />
{:else if pendingKey === 'o' && isOnFriendDetailPage}
<HintModePanel title={$i18n.t('shortcuts.panels.openLink')} prefix={$openFriendLinkModePrefix} itemDescription={$i18n.t('shortcuts.items.link')} />
{:else if pendingKey === 'o' && isOnEncounterDetailPage}
<HintModePanel title={$i18n.t('shortcuts.panels.openFriend')} prefix={$openEncounterFriendModePrefix} itemDescription={$i18n.t('shortcuts.items.friend')} />
{:else if pendingKey === 'f'}
<FilterPanel />
{:else if pendingKey === 'e' && isOnCirclesPage}
Expand Down
20 changes: 20 additions & 0 deletions apps/frontend/src/lib/stores/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,26 @@ export const openEncounterModePrefix = writable<string | null>(null);
*/
export const visibleEncounterIds = writable<string[]>([]);

// =============================================================================
// Encounter Friend Open Mode State (keyboard-driven friend opening on encounter detail)
// =============================================================================

/**
* Tracks if "open encounter friend mode" is active for quick keyboard navigation
* to the friends linked on an encounter detail page
*/
export const isOpenEncounterFriendModeActive = writable(false);

/**
* Tracks the current letter prefix in open encounter friend mode (e.g., 'a', 'b', 'c')
*/
export const openEncounterFriendModePrefix = writable<string | null>(null);

/**
* List of friend IDs linked to the current encounter (for open mode navigation)
*/
export const visibleEncounterFriendIds = writable<string[]>([]);

/**
* Mapping of keyboard keys to filter category field names
*/
Expand Down
Loading