feat: risks and sticky notes
@ -7,6 +7,7 @@
|
|||||||
x?: number;
|
x?: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
zIndex?: number;
|
zIndex?: number;
|
||||||
|
canvasOffset?: { x: number; y: number };
|
||||||
onMove?: (id: number, x: number, y: number) => void;
|
onMove?: (id: number, x: number, y: number) => void;
|
||||||
onClick?: (id: number) => void;
|
onClick?: (id: number) => void;
|
||||||
onDrop?: (id: number, x: number, y: number) => void;
|
onDrop?: (id: number, x: number, y: number) => void;
|
||||||
@ -20,6 +21,7 @@
|
|||||||
x = $bindable(0),
|
x = $bindable(0),
|
||||||
y = $bindable(0),
|
y = $bindable(0),
|
||||||
zIndex = $bindable(0),
|
zIndex = $bindable(0),
|
||||||
|
canvasOffset = { x: 0, y: 0 },
|
||||||
onMove,
|
onMove,
|
||||||
onClick,
|
onClick,
|
||||||
onDrop
|
onDrop
|
||||||
@ -59,8 +61,8 @@
|
|||||||
hasMoved = true;
|
hasMoved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newX = event.clientX - dragOffset.x;
|
const newX = event.clientX - dragOffset.x - canvasOffset.x;
|
||||||
const newY = event.clientY - dragOffset.y;
|
const newY = event.clientY - dragOffset.y - canvasOffset.y;
|
||||||
|
|
||||||
x = newX;
|
x = newX;
|
||||||
y = newY;
|
y = newY;
|
||||||
|
223
src/lib/StickyNote.svelte
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
zIndex: number;
|
||||||
|
color: string;
|
||||||
|
canvasOffset?: { x: number; y: number };
|
||||||
|
onMove?: (id: number, x: number, y: number) => void;
|
||||||
|
onUpdate?: (id: number, text: string) => void;
|
||||||
|
onDelete?: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
id,
|
||||||
|
text = $bindable(''),
|
||||||
|
x = $bindable(0),
|
||||||
|
y = $bindable(0),
|
||||||
|
zIndex = $bindable(0),
|
||||||
|
color,
|
||||||
|
canvasOffset = { x: 0, y: 0 },
|
||||||
|
onMove,
|
||||||
|
onUpdate,
|
||||||
|
onDelete
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let isDragging = $state(false);
|
||||||
|
let dragOffset = $state({ x: 0, y: 0 });
|
||||||
|
let dragStartPos = $state({ x: 0, y: 0 });
|
||||||
|
let hasMoved = $state(false);
|
||||||
|
let noteElement: HTMLDivElement;
|
||||||
|
let textareaElement: HTMLTextAreaElement;
|
||||||
|
let isEditing = $state(false);
|
||||||
|
|
||||||
|
function handleMouseDown(event: MouseEvent) {
|
||||||
|
if (event.button !== 0) return;
|
||||||
|
|
||||||
|
isDragging = true;
|
||||||
|
hasMoved = false;
|
||||||
|
const rect = noteElement.getBoundingClientRect();
|
||||||
|
dragOffset = {
|
||||||
|
x: event.clientX - rect.left,
|
||||||
|
y: event.clientY - rect.top
|
||||||
|
};
|
||||||
|
dragStartPos = {
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY
|
||||||
|
};
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(event: MouseEvent) {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const deltaX = Math.abs(event.clientX - dragStartPos.x);
|
||||||
|
const deltaY = Math.abs(event.clientY - dragStartPos.y);
|
||||||
|
|
||||||
|
if (deltaX > 5 || deltaY > 5) {
|
||||||
|
hasMoved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newX = event.clientX - dragOffset.x - canvasOffset.x;
|
||||||
|
const newY = event.clientY - dragOffset.y - canvasOffset.y;
|
||||||
|
|
||||||
|
x = newX;
|
||||||
|
y = newY;
|
||||||
|
|
||||||
|
onMove?.(id, newX, newY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp() {
|
||||||
|
isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (!hasMoved) {
|
||||||
|
isEditing = true;
|
||||||
|
setTimeout(() => textareaElement?.focus(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTextBlur() {
|
||||||
|
isEditing = false;
|
||||||
|
onUpdate?.(id, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
isEditing = false;
|
||||||
|
onUpdate?.(id, text);
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
isEditing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(event: MouseEvent) {
|
||||||
|
event.stopPropagation();
|
||||||
|
onDelete?.(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isDragging) {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={noteElement}
|
||||||
|
class="sticky-note"
|
||||||
|
class:dragging={isDragging}
|
||||||
|
class:editing={isEditing}
|
||||||
|
style="left: {x}px; top: {y}px; z-index: {zIndex}; background-color: {color};"
|
||||||
|
onmousedown={handleMouseDown}
|
||||||
|
onclick={handleClick}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<button class="delete-btn" onclick={handleDelete}>×</button>
|
||||||
|
|
||||||
|
{#if isEditing}
|
||||||
|
<textarea
|
||||||
|
bind:this={textareaElement}
|
||||||
|
bind:value={text}
|
||||||
|
onblur={handleTextBlur}
|
||||||
|
onkeydown={handleKeyDown}
|
||||||
|
class="note-textarea"
|
||||||
|
placeholder="Enter your note..."
|
||||||
|
></textarea>
|
||||||
|
{:else}
|
||||||
|
<div class="note-text">{text || 'Click to edit'}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.sticky-note {
|
||||||
|
position: absolute;
|
||||||
|
width: 200px;
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-note:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-note.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
z-index: 1000 !important;
|
||||||
|
transform: rotate(2deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-note.dragging:hover {
|
||||||
|
transform: rotate(2deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgba(0, 0, 0, 0.6);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: rgba(255, 0, 0, 0.2);
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-text {
|
||||||
|
min-height: 80px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-text:empty::before {
|
||||||
|
content: 'Click to edit';
|
||||||
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Card from '$lib/Card.svelte';
|
import Card from '$lib/Card.svelte';
|
||||||
|
import StickyNote from '$lib/StickyNote.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
interface CardData {
|
interface CardData {
|
||||||
id: number;
|
id: number;
|
||||||
@ -9,25 +11,128 @@
|
|||||||
y: number;
|
y: number;
|
||||||
zIndex: number;
|
zIndex: number;
|
||||||
inDeck: boolean;
|
inDeck: boolean;
|
||||||
|
isRiskCategory?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardImages = Array.from({ length: 14 }, (_, i) => `front${i + 1}.png`);
|
interface DeckConfig {
|
||||||
const backImage = 'back.png';
|
name: string;
|
||||||
|
backImage: string;
|
||||||
|
cards: string[];
|
||||||
|
riskCategories?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StickyNote {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
zIndex: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deckConfigs: DeckConfig[] = [
|
||||||
|
{
|
||||||
|
name: 'Default',
|
||||||
|
backImage: 'back.png',
|
||||||
|
cards: Array.from({ length: 14 }, (_, i) => `front${i + 1}.png`)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Risk Cards',
|
||||||
|
backImage: 'risks/risiko_back.png',
|
||||||
|
cards: [
|
||||||
|
'risks/altsysteme_und_altlasten1.png',
|
||||||
|
'risks/altsysteme_und_altlasten2.png',
|
||||||
|
'risks/altsysteme_und_altlasten3.png',
|
||||||
|
'risks/altsysteme_und_altlasten4.png',
|
||||||
|
'risks/altsysteme_und_altlasten5.png',
|
||||||
|
'risks/betrieb_und_deployment1.png',
|
||||||
|
'risks/betrieb_und_deployment2.png',
|
||||||
|
'risks/betrieb_und_deployment3.png',
|
||||||
|
'risks/betrieb_und_deployment4.png',
|
||||||
|
'risks/betrieb_und_deployment5.png',
|
||||||
|
'risks/fremdsysteme_und_plattformen1.png',
|
||||||
|
'risks/fremdsysteme_und_plattformen2.png',
|
||||||
|
'risks/fremdsysteme_und_plattformen3.png',
|
||||||
|
'risks/fremdsysteme_und_plattformen4.png',
|
||||||
|
'risks/fremdsysteme_und_plattformen5.png',
|
||||||
|
'risks/kompetenz_und_erfahrung1.png',
|
||||||
|
'risks/kompetenz_und_erfahrung2.png',
|
||||||
|
'risks/kompetenz_und_erfahrung3.png',
|
||||||
|
'risks/kompetenz_und_erfahrung4.png',
|
||||||
|
'risks/kompetenz_und_erfahrung5.png',
|
||||||
|
'risks/orga_und_prozesse1.png',
|
||||||
|
'risks/orga_und_prozesse2.png',
|
||||||
|
'risks/orga_und_prozesse3.png',
|
||||||
|
'risks/orga_und_prozesse4.png',
|
||||||
|
'risks/orga_und_prozesse5.png',
|
||||||
|
'risks/softwareloesung1.png',
|
||||||
|
'risks/softwareloesung2.png',
|
||||||
|
'risks/softwareloesung3.png',
|
||||||
|
'risks/softwareloesung4.png',
|
||||||
|
'risks/softwareloesung5.png',
|
||||||
|
'risks/weiche_faktoren1.png',
|
||||||
|
'risks/weiche_faktoren2.png',
|
||||||
|
'risks/weiche_faktoren3.png',
|
||||||
|
'risks/weiche_faktoren4.png',
|
||||||
|
'risks/weiche_faktoren5.png',
|
||||||
|
'risks/zielsetzung1.png',
|
||||||
|
'risks/zielsetzung2.png',
|
||||||
|
'risks/zielsetzung3.png',
|
||||||
|
'risks/zielsetzung4.png',
|
||||||
|
'risks/zielsetzung5.png'
|
||||||
|
],
|
||||||
|
riskCategories: [
|
||||||
|
'altsysteme_und_altlasten',
|
||||||
|
'betrieb_und_deployment',
|
||||||
|
'fremdsysteme_und_plattformen',
|
||||||
|
'kompetenz_und_erfahrung',
|
||||||
|
'orga_und_prozesse',
|
||||||
|
'softwareloesung',
|
||||||
|
'weiche_faktoren',
|
||||||
|
'zielsetzung'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let selectedDeck = $state(0);
|
||||||
|
let shuffleMode = $state('normal');
|
||||||
|
let currentDeckConfig = $derived(deckConfigs[selectedDeck]);
|
||||||
|
let backImage = $derived(currentDeckConfig.backImage);
|
||||||
|
|
||||||
let deckPosition = $state({ x: 50, y: 300 });
|
let deckPosition = $state({ x: 50, y: 300 });
|
||||||
|
let canvasOffset = $state({ x: 0, y: 0 });
|
||||||
|
let isDragging = $state(false);
|
||||||
|
let dragStart = $state({ x: 0, y: 0 });
|
||||||
|
let dragStartOffset = $state({ x: 0, y: 0 });
|
||||||
|
|
||||||
let cards = $state<CardData[]>(
|
let cards = $state<CardData[]>([]);
|
||||||
cardImages.map((image, index) => ({
|
let maxZIndex = $state(0);
|
||||||
|
let stickyNotes = $state<StickyNote[]>([]);
|
||||||
|
let nextNoteId = $state(1);
|
||||||
|
let noteColors = ['#ffeb3b', '#4caf50', '#2196f3', '#ff9800', '#e91e63', '#9c27b0'];
|
||||||
|
|
||||||
|
function initializeDeck() {
|
||||||
|
const config = deckConfigs[selectedDeck];
|
||||||
|
cards = config.cards.map((image, index) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
frontImage: image,
|
frontImage: image,
|
||||||
flipped: true,
|
flipped: true,
|
||||||
x: deckPosition.x + index * 2,
|
x: deckPosition.x + index * 2,
|
||||||
y: deckPosition.y + index * 2,
|
y: deckPosition.y + index * 2,
|
||||||
zIndex: index,
|
zIndex: index,
|
||||||
inDeck: true
|
inDeck: true,
|
||||||
}))
|
isRiskCategory: selectedDeck === 1 && image.includes('1.png')
|
||||||
);
|
}));
|
||||||
let maxZIndex = $state(cards.length);
|
maxZIndex = cards.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
initializeDeck();
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDeckChange() {
|
||||||
|
initializeDeck();
|
||||||
|
}
|
||||||
|
|
||||||
function handleCardMove(id: number, x: number, y: number) {
|
function handleCardMove(id: number, x: number, y: number) {
|
||||||
const card = cards.find(c => c.id === id);
|
const card = cards.find(c => c.id === id);
|
||||||
@ -41,12 +146,16 @@
|
|||||||
function handleCardDrop(id: number, x: number, y: number) {
|
function handleCardDrop(id: number, x: number, y: number) {
|
||||||
const card = cards.find(c => c.id === id);
|
const card = cards.find(c => c.id === id);
|
||||||
if (card) {
|
if (card) {
|
||||||
// Check if card is dropped on deck area
|
// Calculate card center position (card is 220x300px)
|
||||||
|
const cardCenterX = x + 110;
|
||||||
|
const cardCenterY = y + 150;
|
||||||
|
|
||||||
|
// Check if card center is dropped on deck area
|
||||||
if (
|
if (
|
||||||
x >= deckPosition.x &&
|
cardCenterX >= deckPosition.x &&
|
||||||
x <= deckPosition.x + 220 &&
|
cardCenterX <= deckPosition.x + 220 &&
|
||||||
y >= deckPosition.y &&
|
cardCenterY >= deckPosition.y &&
|
||||||
y <= deckPosition.y + 300 &&
|
cardCenterY <= deckPosition.y + 300 &&
|
||||||
!card.inDeck
|
!card.inDeck
|
||||||
) {
|
) {
|
||||||
// Move card to top of deck
|
// Move card to top of deck
|
||||||
@ -76,6 +185,20 @@
|
|||||||
const deckCards = cards.filter(card => card.inDeck);
|
const deckCards = cards.filter(card => card.inDeck);
|
||||||
if (deckCards.length === 0) return;
|
if (deckCards.length === 0) return;
|
||||||
|
|
||||||
|
if (selectedDeck === 1) {
|
||||||
|
if (shuffleMode === 'shuffle-only-risks') {
|
||||||
|
shuffleOnlyRisks();
|
||||||
|
} else if (shuffleMode === 'shuffle-per-category') {
|
||||||
|
shufflePerCategory();
|
||||||
|
} else {
|
||||||
|
normalShuffle(deckCards);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
normalShuffle(deckCards);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalShuffle(deckCards: CardData[]) {
|
||||||
for (let i = deckCards.length - 1; i > 0; i--) {
|
for (let i = deckCards.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
[deckCards[i], deckCards[j]] = [deckCards[j], deckCards[i]];
|
[deckCards[i], deckCards[j]] = [deckCards[j], deckCards[i]];
|
||||||
@ -89,6 +212,70 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shuffleOnlyRisks() {
|
||||||
|
const deckCards = cards.filter(card => card.inDeck);
|
||||||
|
const riskCategories = deckCards.filter(card => card.isRiskCategory);
|
||||||
|
const riskCards = deckCards.filter(card => !card.isRiskCategory);
|
||||||
|
|
||||||
|
for (let i = riskCards.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[riskCards[i], riskCards[j]] = [riskCards[j], riskCards[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const shuffled = [...riskCategories, ...riskCards];
|
||||||
|
shuffled.reverse();
|
||||||
|
|
||||||
|
const numCategories = riskCategories.length;
|
||||||
|
const totalCards = shuffled.length;
|
||||||
|
|
||||||
|
shuffled.forEach((card, index) => {
|
||||||
|
if (index >= totalCards - numCategories) {
|
||||||
|
const categoryIndex = index - (totalCards - numCategories);
|
||||||
|
card.x = 50 + 330 + (categoryIndex % 6) * 240;
|
||||||
|
card.y = 50 + Math.floor(categoryIndex / 6) * 320;
|
||||||
|
card.zIndex = index;
|
||||||
|
card.flipped = false;
|
||||||
|
card.inDeck = false;
|
||||||
|
} else {
|
||||||
|
card.x = deckPosition.x + index * 2;
|
||||||
|
card.y = deckPosition.y + index * 2;
|
||||||
|
card.zIndex = index;
|
||||||
|
card.flipped = true;
|
||||||
|
card.inDeck = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shufflePerCategory() {
|
||||||
|
const deckCards = cards.filter(card => card.inDeck);
|
||||||
|
const categories = currentDeckConfig.riskCategories || [];
|
||||||
|
const shuffledDeck: CardData[] = [];
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
const categoryCard = deckCards.find(card => card.frontImage.includes(`${category}1.png`));
|
||||||
|
const categoryRisks = deckCards.filter(card =>
|
||||||
|
card.frontImage.includes(category) && !card.frontImage.includes('1.png')
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = categoryRisks.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[categoryRisks[i], categoryRisks[j]] = [categoryRisks[j], categoryRisks[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryCard) shuffledDeck.push(categoryCard);
|
||||||
|
shuffledDeck.push(...categoryRisks);
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffledDeck.reverse();
|
||||||
|
|
||||||
|
shuffledDeck.forEach((card, index) => {
|
||||||
|
card.x = deckPosition.x + index * 2;
|
||||||
|
card.y = deckPosition.y + index * 2;
|
||||||
|
card.zIndex = index;
|
||||||
|
card.flipped = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function collectToDeck() {
|
function collectToDeck() {
|
||||||
cards.forEach((card, index) => {
|
cards.forEach((card, index) => {
|
||||||
card.x = deckPosition.x + index * 2;
|
card.x = deckPosition.x + index * 2;
|
||||||
@ -101,7 +288,7 @@
|
|||||||
|
|
||||||
function dealCards() {
|
function dealCards() {
|
||||||
cards.forEach((card, index) => {
|
cards.forEach((card, index) => {
|
||||||
card.x = 50 + (index % 6) * 240;
|
card.x = 50 + 330 + (index % 6) * 240;
|
||||||
card.y = 50 + Math.floor(index / 6) * 320;
|
card.y = 50 + Math.floor(index / 6) * 320;
|
||||||
card.zIndex = index;
|
card.zIndex = index;
|
||||||
card.flipped = false;
|
card.flipped = false;
|
||||||
@ -114,21 +301,132 @@
|
|||||||
card.flipped = !card.flipped;
|
card.flipped = !card.flipped;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCanvasWheel(event: WheelEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
const scrollSpeed = 1;
|
||||||
|
canvasOffset.x -= event.deltaX * scrollSpeed;
|
||||||
|
canvasOffset.y -= event.deltaY * scrollSpeed;
|
||||||
|
|
||||||
|
canvasOffset.x = Math.max(-2000, Math.min(2000, canvasOffset.x));
|
||||||
|
canvasOffset.y = Math.max(-2000, Math.min(2000, canvasOffset.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCanvasMouseDown(event: MouseEvent) {
|
||||||
|
if (event.button === 1 || event.button === 2) {
|
||||||
|
event.preventDefault();
|
||||||
|
isDragging = true;
|
||||||
|
dragStart.x = event.clientX;
|
||||||
|
dragStart.y = event.clientY;
|
||||||
|
dragStartOffset.x = canvasOffset.x;
|
||||||
|
dragStartOffset.y = canvasOffset.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCanvasMouseMove(event: MouseEvent) {
|
||||||
|
if (isDragging) {
|
||||||
|
event.preventDefault();
|
||||||
|
const deltaX = event.clientX - dragStart.x;
|
||||||
|
const deltaY = event.clientY - dragStart.y;
|
||||||
|
|
||||||
|
canvasOffset.x = Math.max(-2000, Math.min(2000, dragStartOffset.x + deltaX));
|
||||||
|
canvasOffset.y = Math.max(-2000, Math.min(2000, dragStartOffset.y + deltaY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCanvasMouseUp(event: MouseEvent) {
|
||||||
|
if (event.button === 1 || event.button === 2) {
|
||||||
|
isDragging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCanvasContextMenu(event: MouseEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStickyNote() {
|
||||||
|
const randomColor = noteColors[Math.floor(Math.random() * noteColors.length)];
|
||||||
|
const newNote: StickyNote = {
|
||||||
|
id: nextNoteId++,
|
||||||
|
text: 'New note',
|
||||||
|
x: 400 - canvasOffset.x,
|
||||||
|
y: 200 - canvasOffset.y,
|
||||||
|
zIndex: ++maxZIndex,
|
||||||
|
color: randomColor
|
||||||
|
};
|
||||||
|
stickyNotes.push(newNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStickyNote(id: number, text: string) {
|
||||||
|
const note = stickyNotes.find(n => n.id === id);
|
||||||
|
if (note) {
|
||||||
|
note.text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveStickyNote(id: number, x: number, y: number) {
|
||||||
|
const note = stickyNotes.find(n => n.id === id);
|
||||||
|
if (note) {
|
||||||
|
note.x = x;
|
||||||
|
note.y = y;
|
||||||
|
note.zIndex = ++maxZIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteStickyNote(id: number) {
|
||||||
|
const index = stickyNotes.findIndex(n => n.id === id);
|
||||||
|
if (index >= 0) {
|
||||||
|
stickyNotes.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="game-container">
|
<div class="game-container">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="deck-select">Deck:</label>
|
||||||
|
<select id="deck-select" bind:value={selectedDeck} onchange={handleDeckChange}>
|
||||||
|
{#each deckConfigs as config, index}
|
||||||
|
<option value={index}>{config.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if selectedDeck === 1}
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="shuffle-mode">Shuffle:</label>
|
||||||
|
<select id="shuffle-mode" bind:value={shuffleMode}>
|
||||||
|
<option value="normal">Normal</option>
|
||||||
|
<option value="shuffle-only-risks">Only Risks</option>
|
||||||
|
<option value="shuffle-per-category">Per Category</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<button onclick={collectToDeck}>Collect to Deck</button>
|
<button onclick={collectToDeck}>Collect to Deck</button>
|
||||||
<button onclick={shuffleDeck}>Shuffle Deck</button>
|
<button onclick={shuffleDeck}>Shuffle Deck</button>
|
||||||
<button onclick={dealCards}>Deal Cards</button>
|
<button onclick={dealCards}>Deal Cards</button>
|
||||||
<button onclick={flipAllCards}>Flip All</button>
|
<button onclick={flipAllCards}>Flip All</button>
|
||||||
|
<button onclick={addStickyNote}>Add Note</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="game-area">
|
<div
|
||||||
|
class="game-area"
|
||||||
|
onwheel={handleCanvasWheel}
|
||||||
|
onmousedown={handleCanvasMouseDown}
|
||||||
|
onmousemove={handleCanvasMouseMove}
|
||||||
|
onmouseup={handleCanvasMouseUp}
|
||||||
|
oncontextmenu={handleCanvasContextMenu}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="canvas-content"
|
||||||
|
style="transform: translate({canvasOffset.x}px, {canvasOffset.y}px);"
|
||||||
|
>
|
||||||
{#each cards as card (card.id)}
|
{#each cards as card (card.id)}
|
||||||
<Card
|
<Card
|
||||||
{...card}
|
{...card}
|
||||||
backImage={backImage}
|
backImage={card.isRiskCategory ? 'risks/risikocat_back.png' : backImage}
|
||||||
|
canvasOffset={canvasOffset}
|
||||||
bind:flipped={card.flipped}
|
bind:flipped={card.flipped}
|
||||||
bind:x={card.x}
|
bind:x={card.x}
|
||||||
bind:y={card.y}
|
bind:y={card.y}
|
||||||
@ -139,6 +437,20 @@
|
|||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
{#each stickyNotes as note (note.id)}
|
||||||
|
<StickyNote
|
||||||
|
{...note}
|
||||||
|
canvasOffset={canvasOffset}
|
||||||
|
bind:text={note.text}
|
||||||
|
bind:x={note.x}
|
||||||
|
bind:y={note.y}
|
||||||
|
bind:zIndex={note.zIndex}
|
||||||
|
onMove={moveStickyNote}
|
||||||
|
onUpdate={updateStickyNote}
|
||||||
|
onDelete={deleteStickyNote}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="deck-area"
|
class="deck-area"
|
||||||
style="left: {deckPosition.x}px; top: {deckPosition.y}px;"
|
style="left: {deckPosition.x}px; top: {deckPosition.y}px;"
|
||||||
@ -146,6 +458,7 @@
|
|||||||
Deck
|
Deck
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -164,6 +477,33 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 27px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group select {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls button {
|
.controls button {
|
||||||
@ -187,6 +527,20 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-area:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-content {
|
||||||
|
position: relative;
|
||||||
|
width: 500vw;
|
||||||
|
height: 500vh;
|
||||||
|
min-width: 500vw;
|
||||||
|
min-height: 500vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deck-area {
|
.deck-area {
|
||||||
|
BIN
static/risks/altsysteme_und_altlasten1.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
static/risks/altsysteme_und_altlasten2.png
Normal file
After Width: | Height: | Size: 232 KiB |
BIN
static/risks/altsysteme_und_altlasten3.png
Normal file
After Width: | Height: | Size: 358 KiB |
BIN
static/risks/altsysteme_und_altlasten4.png
Normal file
After Width: | Height: | Size: 316 KiB |
BIN
static/risks/altsysteme_und_altlasten5.png
Normal file
After Width: | Height: | Size: 351 KiB |
BIN
static/risks/betrieb_und_deployment1.png
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
static/risks/betrieb_und_deployment2.png
Normal file
After Width: | Height: | Size: 265 KiB |
BIN
static/risks/betrieb_und_deployment3.png
Normal file
After Width: | Height: | Size: 488 KiB |
BIN
static/risks/betrieb_und_deployment4.png
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
static/risks/betrieb_und_deployment5.png
Normal file
After Width: | Height: | Size: 387 KiB |
BIN
static/risks/fremdsysteme_und_plattformen1.png
Normal file
After Width: | Height: | Size: 189 KiB |
BIN
static/risks/fremdsysteme_und_plattformen2.png
Normal file
After Width: | Height: | Size: 237 KiB |
BIN
static/risks/fremdsysteme_und_plattformen3.png
Normal file
After Width: | Height: | Size: 381 KiB |
BIN
static/risks/fremdsysteme_und_plattformen4.png
Normal file
After Width: | Height: | Size: 371 KiB |
BIN
static/risks/fremdsysteme_und_plattformen5.png
Normal file
After Width: | Height: | Size: 296 KiB |
BIN
static/risks/kompetenz_und_erfahrung1.png
Normal file
After Width: | Height: | Size: 178 KiB |
BIN
static/risks/kompetenz_und_erfahrung2.png
Normal file
After Width: | Height: | Size: 290 KiB |
BIN
static/risks/kompetenz_und_erfahrung3.png
Normal file
After Width: | Height: | Size: 327 KiB |
BIN
static/risks/kompetenz_und_erfahrung4.png
Normal file
After Width: | Height: | Size: 378 KiB |
BIN
static/risks/kompetenz_und_erfahrung5.png
Normal file
After Width: | Height: | Size: 318 KiB |
BIN
static/risks/orga_und_prozesse1.png
Normal file
After Width: | Height: | Size: 159 KiB |
BIN
static/risks/orga_und_prozesse2.png
Normal file
After Width: | Height: | Size: 310 KiB |
BIN
static/risks/orga_und_prozesse3.png
Normal file
After Width: | Height: | Size: 321 KiB |
BIN
static/risks/orga_und_prozesse4.png
Normal file
After Width: | Height: | Size: 575 KiB |
BIN
static/risks/orga_und_prozesse5.png
Normal file
After Width: | Height: | Size: 366 KiB |
BIN
static/risks/risiko_back.png
Normal file
After Width: | Height: | Size: 474 KiB |
BIN
static/risks/risikocat_back.png
Normal file
After Width: | Height: | Size: 295 KiB |
BIN
static/risks/softwareloesung1.png
Normal file
After Width: | Height: | Size: 165 KiB |
BIN
static/risks/softwareloesung2.png
Normal file
After Width: | Height: | Size: 492 KiB |
BIN
static/risks/softwareloesung3.png
Normal file
After Width: | Height: | Size: 368 KiB |
BIN
static/risks/softwareloesung4.png
Normal file
After Width: | Height: | Size: 287 KiB |
BIN
static/risks/softwareloesung5.png
Normal file
After Width: | Height: | Size: 276 KiB |
BIN
static/risks/weiche_faktoren1.png
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
static/risks/weiche_faktoren2.png
Normal file
After Width: | Height: | Size: 265 KiB |
BIN
static/risks/weiche_faktoren3.png
Normal file
After Width: | Height: | Size: 291 KiB |
BIN
static/risks/weiche_faktoren4.png
Normal file
After Width: | Height: | Size: 293 KiB |
BIN
static/risks/weiche_faktoren5.png
Normal file
After Width: | Height: | Size: 280 KiB |
BIN
static/risks/zielsetzung1.png
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
static/risks/zielsetzung2.png
Normal file
After Width: | Height: | Size: 342 KiB |
BIN
static/risks/zielsetzung3.png
Normal file
After Width: | Height: | Size: 262 KiB |
BIN
static/risks/zielsetzung4.png
Normal file
After Width: | Height: | Size: 310 KiB |
BIN
static/risks/zielsetzung5.png
Normal file
After Width: | Height: | Size: 288 KiB |