mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 01:01:09 +02:00
refactor(contacts): remove network and new contact pages
- Remove NetworkToolbar and NetworkToolbarContent components - Remove standalone network visualization page - Remove dedicated new contact page Network view integrated into main contacts; new contact via modal. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d353fb076f
commit
c7a9e88d13
4 changed files with 0 additions and 1327 deletions
|
|
@ -1,28 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { ExpandableToolbar } from '@manacore/shared-ui';
|
||||
import NetworkToolbarContent from './NetworkToolbarContent.svelte';
|
||||
import { networkStore } from '$lib/stores/network.svelte';
|
||||
|
||||
interface Props {
|
||||
isSidebarMode?: boolean;
|
||||
}
|
||||
|
||||
let { isSidebarMode = false }: Props = $props();
|
||||
|
||||
// Use store for collapsed state
|
||||
let isCollapsed = $derived(networkStore.isToolbarCollapsed);
|
||||
|
||||
function handleCollapsedChange(collapsed: boolean) {
|
||||
networkStore.setToolbarCollapsed(collapsed);
|
||||
}
|
||||
</script>
|
||||
|
||||
<ExpandableToolbar
|
||||
{isCollapsed}
|
||||
onCollapsedChange={handleCollapsedChange}
|
||||
{isSidebarMode}
|
||||
collapsedTitle="Netzwerk-Optionen"
|
||||
expandedTitle="Schließen"
|
||||
>
|
||||
<NetworkToolbarContent />
|
||||
</ExpandableToolbar>
|
||||
|
|
@ -1,305 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { ZoomIn, ZoomOut, RotateCcw, Focus, X } from 'lucide-svelte';
|
||||
import { networkStore } from '$lib/stores/network.svelte';
|
||||
|
||||
let strengthValue = $state(networkStore.minStrength);
|
||||
|
||||
// Sync strength with store
|
||||
$effect(() => {
|
||||
strengthValue = networkStore.minStrength;
|
||||
});
|
||||
|
||||
function handleTagChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
networkStore.setFilterTag(target.value || null);
|
||||
}
|
||||
|
||||
function handleCompanyChange(event: Event) {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
networkStore.setFilterCompany(target.value || null);
|
||||
}
|
||||
|
||||
function handleStrengthChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
strengthValue = parseInt(target.value, 10);
|
||||
networkStore.setMinStrength(strengthValue);
|
||||
}
|
||||
|
||||
function clearAllFilters() {
|
||||
strengthValue = 0;
|
||||
networkStore.clearFilters();
|
||||
}
|
||||
|
||||
const hasActiveFilters = $derived(
|
||||
networkStore.filterTagId || networkStore.filterCompany || networkStore.minStrength > 0
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="toolbar-content-inner">
|
||||
<!-- Tag Filter -->
|
||||
{#if networkStore.uniqueTags.length > 0}
|
||||
<div class="filter-group">
|
||||
<select
|
||||
onchange={handleTagChange}
|
||||
value={networkStore.filterTagId || ''}
|
||||
class="filter-select"
|
||||
title="Tag filtern"
|
||||
>
|
||||
<option value="">Alle Tags</option>
|
||||
{#each networkStore.uniqueTags as tag}
|
||||
<option value={tag.id}>{tag.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Company Filter -->
|
||||
{#if networkStore.uniqueCompanies.length > 0}
|
||||
<div class="filter-group">
|
||||
<select
|
||||
onchange={handleCompanyChange}
|
||||
value={networkStore.filterCompany || ''}
|
||||
class="filter-select"
|
||||
title="Firma filtern"
|
||||
>
|
||||
<option value="">Alle Firmen</option>
|
||||
{#each networkStore.uniqueCompanies as company}
|
||||
<option value={company}>{company}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<!-- Strength Filter -->
|
||||
<div class="strength-group">
|
||||
<label for="network-strength-filter" class="strength-label">
|
||||
Stärke: {strengthValue}%
|
||||
</label>
|
||||
<input
|
||||
id="network-strength-filter"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="10"
|
||||
value={strengthValue}
|
||||
oninput={handleStrengthChange}
|
||||
class="strength-slider"
|
||||
title="Mindest-Verbindungsstärke"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<!-- Zoom Controls -->
|
||||
<div class="zoom-controls">
|
||||
<button
|
||||
onclick={() => networkStore.zoomIn()}
|
||||
class="control-btn"
|
||||
aria-label="Vergrößern"
|
||||
title="Vergrößern (+)"
|
||||
>
|
||||
<ZoomIn size={16} />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => networkStore.zoomOut()}
|
||||
class="control-btn"
|
||||
aria-label="Verkleinern"
|
||||
title="Verkleinern (-)"
|
||||
>
|
||||
<ZoomOut size={16} />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => networkStore.resetZoom()}
|
||||
class="control-btn"
|
||||
aria-label="Ansicht zurücksetzen"
|
||||
title="Zurücksetzen (0)"
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => networkStore.focusOnSelected()}
|
||||
class="control-btn"
|
||||
aria-label="Auf Auswahl fokussieren"
|
||||
title="Fokus auf Auswahl (F)"
|
||||
>
|
||||
<Focus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Clear Filters -->
|
||||
{#if hasActiveFilters}
|
||||
<div class="toolbar-divider"></div>
|
||||
<button onclick={clearAllFilters} class="clear-btn" title="Filter löschen">
|
||||
<X size={14} />
|
||||
<span>Filter löschen</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats">
|
||||
<span class="stat">{networkStore.nodes.length} Kontakte</span>
|
||||
<span class="stat-divider">•</span>
|
||||
<span class="stat">{networkStore.links.length} Verbindungen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toolbar-content-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 1.5rem;
|
||||
background: hsl(var(--color-border));
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.375rem 0.625rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-background));
|
||||
color: hsl(var(--color-foreground));
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.filter-select:hover {
|
||||
border-color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.strength-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.strength-label {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.strength-slider {
|
||||
width: 80px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: hsl(var(--color-muted));
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.strength-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: hsl(var(--color-primary));
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.strength-slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.strength-slider::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: hsl(var(--color-primary));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
cursor: pointer;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: hsl(var(--color-muted));
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: hsl(var(--destructive));
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: hsl(var(--destructive) / 0.15);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.stats {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.strength-group {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.strength-slider {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,747 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { contactsApi } from '$lib/api/contacts';
|
||||
import '$lib/i18n';
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
let firstName = $state('');
|
||||
let lastName = $state('');
|
||||
let email = $state('');
|
||||
let phone = $state('');
|
||||
let mobile = $state('');
|
||||
let company = $state('');
|
||||
let jobTitle = $state('');
|
||||
let street = $state('');
|
||||
let city = $state('');
|
||||
let postalCode = $state('');
|
||||
let country = $state('');
|
||||
let notes = $state('');
|
||||
|
||||
const initials = $derived(() => {
|
||||
const f = firstName?.[0] || '';
|
||||
const l = lastName?.[0] || '';
|
||||
return (f + l).toUpperCase() || '?';
|
||||
});
|
||||
|
||||
const displayName = $derived(() => {
|
||||
if (firstName || lastName) {
|
||||
return [firstName, lastName].filter(Boolean).join(' ');
|
||||
}
|
||||
return email || 'Neuer Kontakt';
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!firstName && !lastName && !email) {
|
||||
error = 'Bitte mindestens Name oder E-Mail angeben';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
await contactsApi.create({
|
||||
firstName: firstName || null,
|
||||
lastName: lastName || null,
|
||||
email: email || null,
|
||||
phone: phone || null,
|
||||
mobile: mobile || null,
|
||||
company: company || null,
|
||||
jobTitle: jobTitle || null,
|
||||
street: street || null,
|
||||
city: city || null,
|
||||
postalCode: postalCode || null,
|
||||
country: country || null,
|
||||
notes: notes || null,
|
||||
});
|
||||
goto('/');
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Erstellen des Kontakts';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Neuer Kontakt - Contacts</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="page-container">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<a href="/" class="back-button" aria-label="Zurück">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="title">Neuer Kontakt</h1>
|
||||
<div class="header-spacer"></div>
|
||||
</header>
|
||||
|
||||
<!-- Avatar Preview -->
|
||||
<div class="avatar-section">
|
||||
<div class="avatar-wrapper">
|
||||
<div class="avatar-circle">
|
||||
{initials()}
|
||||
</div>
|
||||
<button type="button" class="avatar-edit-btn" aria-label="Foto hinzufügen">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="preview-name">{displayName()}</p>
|
||||
{#if company || jobTitle}
|
||||
<p class="preview-subtitle">{[jobTitle, company].filter(Boolean).join(' bei ')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="error-banner" role="alert">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
class="form"
|
||||
>
|
||||
<!-- Name Section -->
|
||||
<section class="form-section">
|
||||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Name</h2>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="firstName" class="label">Vorname</label>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
bind:value={firstName}
|
||||
class="input"
|
||||
placeholder="Max"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="lastName" class="label">Nachname</label>
|
||||
<input
|
||||
id="lastName"
|
||||
type="text"
|
||||
bind:value={lastName}
|
||||
class="input"
|
||||
placeholder="Mustermann"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<section class="form-section">
|
||||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Kontakt</h2>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="email" class="label">E-Mail</label>
|
||||
<div class="input-with-icon">
|
||||
<svg class="input-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="input input-padded"
|
||||
placeholder="max@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="phone" class="label">Telefon</label>
|
||||
<div class="input-with-icon">
|
||||
<svg class="input-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
id="phone"
|
||||
type="tel"
|
||||
bind:value={phone}
|
||||
class="input input-padded"
|
||||
placeholder="+49 123 456789"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="mobile" class="label">Mobil</label>
|
||||
<div class="input-with-icon">
|
||||
<svg class="input-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
id="mobile"
|
||||
type="tel"
|
||||
bind:value={mobile}
|
||||
class="input input-padded"
|
||||
placeholder="+49 170 1234567"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Work Section -->
|
||||
<section class="form-section">
|
||||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Arbeit</h2>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="company" class="label">Firma</label>
|
||||
<input
|
||||
id="company"
|
||||
type="text"
|
||||
bind:value={company}
|
||||
class="input"
|
||||
placeholder="Musterfirma GmbH"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="jobTitle" class="label">Position</label>
|
||||
<input
|
||||
id="jobTitle"
|
||||
type="text"
|
||||
bind:value={jobTitle}
|
||||
class="input"
|
||||
placeholder="Geschäftsführer"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Address Section -->
|
||||
<section class="form-section">
|
||||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Adresse</h2>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="street" class="label">Straße & Hausnummer</label>
|
||||
<input
|
||||
id="street"
|
||||
type="text"
|
||||
bind:value={street}
|
||||
class="input"
|
||||
placeholder="Musterstraße 1"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-grid form-grid-3">
|
||||
<div class="form-field">
|
||||
<label for="postalCode" class="label">PLZ</label>
|
||||
<input
|
||||
id="postalCode"
|
||||
type="text"
|
||||
bind:value={postalCode}
|
||||
class="input"
|
||||
placeholder="12345"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field col-span-2">
|
||||
<label for="city" class="label">Stadt</label>
|
||||
<input id="city" type="text" bind:value={city} class="input" placeholder="Berlin" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="country" class="label">Land</label>
|
||||
<input
|
||||
id="country"
|
||||
type="text"
|
||||
bind:value={country}
|
||||
class="input"
|
||||
placeholder="Deutschland"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Notes Section -->
|
||||
<section class="form-section">
|
||||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Notizen</h2>
|
||||
</div>
|
||||
<textarea
|
||||
bind:value={notes}
|
||||
rows="4"
|
||||
class="input textarea"
|
||||
placeholder="Notizen zum Kontakt..."
|
||||
></textarea>
|
||||
</section>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="actions">
|
||||
<a href="/" class="btn btn-secondary"> Abbrechen </a>
|
||||
<button type="submit" disabled={loading} class="btn btn-primary">
|
||||
{#if loading}
|
||||
<svg class="spinner" viewBox="0 0 24 24" fill="none">
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-opacity="0.25"
|
||||
/>
|
||||
<path
|
||||
d="M12 2a10 10 0 0 1 10 10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>Speichern...</span>
|
||||
{:else}
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span>Kontakt speichern</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-container {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem 2rem;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: hsl(var(--color-background));
|
||||
z-index: 10;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
background: hsl(var(--color-muted));
|
||||
color: hsl(var(--color-foreground));
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: hsl(var(--color-surface-hover));
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.header-spacer {
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
/* Avatar Section */
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1.5rem 0 2rem;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.avatar-circle {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--color-primary)) 0%,
|
||||
hsl(var(--color-primary) / 0.7) 100%
|
||||
);
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 24px hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.avatar-edit-btn {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: hsl(var(--color-surface));
|
||||
border: 2px solid hsl(var(--color-background));
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.avatar-edit-btn:hover {
|
||||
background: hsl(var(--color-surface-hover));
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.preview-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
text-align: center;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Error Banner */
|
||||
.error-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: hsl(var(--color-error) / 0.1);
|
||||
border: 1px solid hsl(var(--color-error) / 0.3);
|
||||
border-radius: 0.75rem;
|
||||
color: hsl(var(--color-error));
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Form Section */
|
||||
.form-section {
|
||||
background: hsl(var(--color-surface));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 1rem;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid hsl(var(--color-border) / 0.5);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-primary) / 0.1);
|
||||
color: hsl(var(--color-primary));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.section-icon svg {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
/* Form Fields */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-grid-3 {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
|
||||
.col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1.5px solid hsl(var(--color-border));
|
||||
border-radius: 0.625rem;
|
||||
background: hsl(var(--color-input));
|
||||
color: hsl(var(--color-foreground));
|
||||
font-size: 0.9375rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--color-primary));
|
||||
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.1);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: hsl(var(--color-muted-foreground) / 0.6);
|
||||
}
|
||||
|
||||
.input-with-icon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 0.875rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.input-padded {
|
||||
padding-left: 2.75rem;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
resize: none;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.875rem 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
box-shadow: 0 4px 12px hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px hsl(var(--color-primary) / 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: hsl(var(--color-muted));
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: hsl(var(--color-surface-hover));
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.icon-sm {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.spinner {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-grid-3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.col-span-2 {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { networkStore, type SimulationNode } from '$lib/stores/network.svelte';
|
||||
import { contactsFilterStore } from '$lib/stores/filter.svelte';
|
||||
import { NetworkGraph } from '@manacore/shared-ui';
|
||||
import ContactDetailModal from '$lib/components/ContactDetailModal.svelte';
|
||||
import { NetworkGraphSkeleton } from '$lib/components/skeletons';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Sync global search to network store
|
||||
$effect(() => {
|
||||
networkStore.setSearch(contactsFilterStore.searchQuery);
|
||||
});
|
||||
|
||||
// Refocus view when search results change
|
||||
let previousNodeCount = $state(0);
|
||||
$effect(() => {
|
||||
const currentNodeCount = networkStore.nodes.length;
|
||||
const hasSearch = contactsFilterStore.searchQuery.length > 0;
|
||||
|
||||
// If search is active and node count changed, reset zoom to show all results
|
||||
if (hasSearch && currentNodeCount !== previousNodeCount && currentNodeCount > 0) {
|
||||
setTimeout(() => {
|
||||
graphComponent?.resetZoom();
|
||||
}, 100);
|
||||
}
|
||||
previousNodeCount = currentNodeCount;
|
||||
});
|
||||
|
||||
let graphComponent: NetworkGraph;
|
||||
let graphContainer: HTMLDivElement;
|
||||
|
||||
function handleNodeClick(node: SimulationNode) {
|
||||
// Select node (highlight connections and show detail sidebar)
|
||||
networkStore.selectNode(node.id);
|
||||
}
|
||||
|
||||
function handleNodeDoubleClick(node: SimulationNode) {
|
||||
// Navigate to contact detail page
|
||||
goto(`/contacts/${node.id}`);
|
||||
}
|
||||
|
||||
function handleBackgroundClick() {
|
||||
networkStore.selectNode(null);
|
||||
}
|
||||
|
||||
function handleCloseSidebar() {
|
||||
networkStore.selectNode(null);
|
||||
}
|
||||
|
||||
function handleDragStart(node: SimulationNode) {
|
||||
networkStore.fixNode(node.id, node.x ?? 0, node.y ?? 0);
|
||||
networkStore.reheatSimulation();
|
||||
}
|
||||
|
||||
function handleDrag(node: SimulationNode, x: number, y: number) {
|
||||
networkStore.fixNode(node.id, x, y);
|
||||
}
|
||||
|
||||
function handleDragEnd(node: SimulationNode) {
|
||||
networkStore.releaseNode(node.id);
|
||||
}
|
||||
|
||||
// Register graph component with store when it changes
|
||||
$effect(() => {
|
||||
networkStore.setGraphComponent(graphComponent);
|
||||
});
|
||||
|
||||
// Initialize simulation when data is loaded and container is ready
|
||||
$effect(() => {
|
||||
if (!networkStore.loading && networkStore.allNodes.length > 0 && graphContainer) {
|
||||
const rect = graphContainer.getBoundingClientRect();
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
networkStore.initSimulation(rect.width, rect.height);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
networkStore.loadGraph();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
networkStore.setGraphComponent(null);
|
||||
networkStore.stopSimulation();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Netzwerk - Contacts</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="network-page">
|
||||
<!-- Error Banner -->
|
||||
{#if networkStore.error}
|
||||
<div class="error-banner" role="alert">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{networkStore.error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="graph-container" bind:this={graphContainer}>
|
||||
{#if networkStore.loading}
|
||||
<NetworkGraphSkeleton />
|
||||
{:else}
|
||||
<NetworkGraph
|
||||
bind:this={graphComponent}
|
||||
nodes={networkStore.nodes}
|
||||
links={networkStore.links}
|
||||
selectedNodeId={networkStore.selectedNodeId}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
onBackgroundClick={handleBackgroundClick}
|
||||
onDragStart={handleDragStart}
|
||||
onDrag={handleDrag}
|
||||
onDragEnd={handleDragEnd}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Contact Detail Modal as Sidebar -->
|
||||
{#if networkStore.selectedNodeId}
|
||||
<div class="modal-sidebar-wrapper">
|
||||
<ContactDetailModal contactId={networkStore.selectedNodeId} onClose={handleCloseSidebar} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.network-page {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Error Banner */
|
||||
.error-banner {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
border: 1px solid hsl(var(--destructive) / 0.3);
|
||||
border-radius: 0.875rem;
|
||||
color: hsl(var(--destructive));
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
/* Graph Container - Full screen */
|
||||
.graph-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Modal Sidebar Wrapper - Override modal positioning */
|
||||
.modal-sidebar-wrapper {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
bottom: calc(
|
||||
200px + env(safe-area-inset-bottom)
|
||||
); /* Above controls + QuickInputBar + PillNav */
|
||||
width: 400px;
|
||||
max-width: calc(100vw - 2rem);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
/* Override the modal styles when inside the sidebar wrapper */
|
||||
.modal-sidebar-wrapper :global(.modal-backdrop) {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
padding: 0;
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-sidebar-wrapper :global(.modal-container) {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
border-radius: 1rem;
|
||||
background: hsl(var(--card) / 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid hsl(var(--border) / 0.5);
|
||||
animation: slideInRight 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.modal-sidebar-wrapper {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.modal-sidebar-wrapper :global(.modal-container) {
|
||||
border-radius: 1rem 1rem 0 0;
|
||||
animation: slideInUp 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue