mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-14 03:00:46 +02:00
Initial open source release
This commit is contained in:
commit
11cf01f08e
488 files changed
+97180
No files matched your search
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import { onClickCopy } from '$lib/utils/common';
|
||||
|
||||
export let text;
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="hover:bg-gray-100 px-2 py-1 rounded-md transition-colors w-full text-left text-ellipsis overflow-hidden"
|
||||
title={text}
|
||||
on:click={() => onClickCopy(text)}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
/** @type {*} */
|
||||
export let page = null;
|
||||
export let plural;
|
||||
export let colspan = 100;
|
||||
</script>
|
||||
|
||||
<tr class="text-center bg-pleasant-gray">
|
||||
<td class="p-24" {colspan}>
|
||||
{#if page === 1}
|
||||
<p class="text-lg text-gray-600">No {plural} found</p>
|
||||
{:else}
|
||||
<p class="text-lg text-gray-600">No more results found</p>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
import { toEvent } from '$lib/utils/events';
|
||||
import { onMount } from 'svelte';
|
||||
export let eventName;
|
||||
|
||||
let event = { name: '', priority: null, color: null };
|
||||
|
||||
onMount(() => {
|
||||
event = toEvent(eventName);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex items-center text-ellipsis">
|
||||
<div class="w-4 h-4 {event.color} mr-2 rounded-sm"></div>
|
||||
{event.name}
|
||||
</div>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script>
|
||||
import EmptyTableResult from './EmptyTableResult.svelte';
|
||||
import Pagination from '../Pagination.svelte';
|
||||
import TableHeader from './TableHeader.svelte';
|
||||
import Search from '$lib/components/Search.svelte';
|
||||
import Select from '$lib/components/Select.svelte';
|
||||
import { afterUpdate, onMount } from 'svelte';
|
||||
import TableCell from './TableCell.svelte';
|
||||
import TableRow from './TableRow.svelte';
|
||||
import TableCellEmpty from './TableCellEmpty.svelte';
|
||||
import TableCellAction from './TableCellAction.svelte';
|
||||
import GhostText from '../GhostText.svelte';
|
||||
import { scrollBarClassesHorizontal } from '$lib/utils/scrollbar';
|
||||
|
||||
/** @type {Array<string>|*} */
|
||||
export let columns = [];
|
||||
/** @type {boolean} */
|
||||
export let hasData;
|
||||
/** @type {string} */
|
||||
export let plural;
|
||||
/** @type {*} */
|
||||
export let pagination = null;
|
||||
export let sortable = [];
|
||||
// key value map that should be switched on when selecting a sort by
|
||||
export let hasActions = true;
|
||||
export let isGhost = false;
|
||||
|
||||
let tableWrapper = null;
|
||||
let columnsLength = columns.length;
|
||||
|
||||
let rowsLength = 0;
|
||||
|
||||
afterUpdate(() => {
|
||||
const elements = tableWrapper?.querySelectorAll('table > tr.table-row');
|
||||
rowsLength = elements?.length ?? 0;
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (!pagination && sortable?.length) {
|
||||
console.warn('You need to pass a pagination object to make the column sortable');
|
||||
}
|
||||
columnsLength = columns.length + 2;
|
||||
});
|
||||
|
||||
let currentPage = pagination && pagination.currentPage;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center pb-4">
|
||||
{#if pagination}
|
||||
<Select {pagination}></Select>
|
||||
<Search {pagination}></Search>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
bind:this={tableWrapper}
|
||||
class="
|
||||
border-2 rounded-md px-4 py-4 overflow-x-auto
|
||||
{scrollBarClassesHorizontal}"
|
||||
>
|
||||
<table class="w-full table-fixed" class:animate-pulse={isGhost}>
|
||||
<TableHeader {isGhost} {columns} {sortable} {hasActions} {pagination} />
|
||||
{#if !hasData && !isGhost}
|
||||
<EmptyTableResult page={currentPage} {plural} colspan={columnsLength} />
|
||||
{/if}
|
||||
{#if !isGhost}
|
||||
<slot />
|
||||
{:else}
|
||||
{#each Array(rowsLength || pagination?.perPage) as _, row}
|
||||
<TableRow>
|
||||
{#each columns as column}
|
||||
<TableCell>
|
||||
<GhostText />
|
||||
</TableCell>
|
||||
{/each}
|
||||
<TableCellEmpty />
|
||||
<TableCellAction>
|
||||
<GhostText square center />
|
||||
</TableCellAction>
|
||||
</TableRow>
|
||||
{/each}
|
||||
{/if}
|
||||
</table>
|
||||
</div>
|
||||
{#if pagination}
|
||||
<Pagination paginator={pagination} />
|
||||
{:else}
|
||||
<div class="flex items-center mb-8 mt-4" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!-- TableCell.svelte -->
|
||||
<script>
|
||||
import Datetime from '../Datetime.svelte';
|
||||
import RelativeTime from '../RelativeTime.svelte';
|
||||
|
||||
/** @type {string} */
|
||||
export let value = '';
|
||||
export let isDate = false;
|
||||
export let hideHours = false;
|
||||
export let isRelative = false; // new prop to toggle relative time
|
||||
</script>
|
||||
|
||||
<td
|
||||
class={`pl-4 font-regular text-slate-600 text-ellipsis whitespace-nowrap overflow-hidden pr-4`}
|
||||
title={isDate ? '' : value}
|
||||
>
|
||||
{#if value}
|
||||
{#if isDate}
|
||||
{#if isRelative}
|
||||
<RelativeTime {value} />
|
||||
{:else}
|
||||
<Datetime {value} {hideHours} />
|
||||
{/if}
|
||||
{:else}
|
||||
{value ?? ''}
|
||||
{/if}
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
</td>
|
||||
@@ -0,0 +1,5 @@
|
||||
<td class="pl-4 w-48 text-center border: hidden;">
|
||||
<p class="font-regular text-slate-600">
|
||||
<slot />
|
||||
</p>
|
||||
</td>
|
||||
@@ -0,0 +1,5 @@
|
||||
<td class="pl-4 w-40 text-center border: hidden;">
|
||||
<p class="font-regular text-slate-600 flex justify-center">
|
||||
<slot />
|
||||
</p>
|
||||
</td>
|
||||
@@ -0,0 +1,3 @@
|
||||
<td class="pl-4 w-4 border: hidden;">
|
||||
|
||||
</td>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import TableDropDownButton from './TableDropDownButton.svelte';
|
||||
|
||||
export let disabled = false;
|
||||
export let title = 'Copy';
|
||||
|
||||
if (disabled && !title) {
|
||||
title = 'Disabled';
|
||||
}
|
||||
</script>
|
||||
|
||||
<TableDropDownButton name={'Copy'} {title} on:click {disabled} />
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
export let disabled = false;
|
||||
export let title = '';
|
||||
export let name = 'Delete';
|
||||
if (disabled && !title) {
|
||||
title = 'Delete is disabled';
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if disabled}
|
||||
<button class="px py text-slate-300 cursor-not-allowed" {disabled} {title}>
|
||||
<p class="ml-2 text-left">{name}</p>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="px py-1 text-slate-600 hover:bg-red-400 hover:text-white cursor-pointer"
|
||||
on:click
|
||||
{title}
|
||||
>
|
||||
<p class="ml-2 text-left">{name}</p>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script>
|
||||
export let disabled = false;
|
||||
export let title = '';
|
||||
export let name;
|
||||
if (disabled && !title) {
|
||||
title = 'Disabled';
|
||||
}
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.target.click();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if disabled}
|
||||
<button class="px py-1 text-slate-300 cursor-not-allowed" {disabled} {title}>
|
||||
<p class="ml-2 text-left">{name}</p>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="px py-1 text-slate-600 hover:bg-highlight-blue hover:text-white cursor-pointer"
|
||||
on:click
|
||||
on:keydown={handleKeydown}
|
||||
{title}
|
||||
>
|
||||
<p class="ml-2 text-left">{name}</p>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -0,0 +1,133 @@
|
||||
<script>
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { activeFormElement } from '$lib/store/activeFormElement';
|
||||
import { scrollBarClassesVertical } from '$lib/utils/scrollbar';
|
||||
|
||||
let isMenuVisible = false;
|
||||
let menuX = 0;
|
||||
let menuY = 0;
|
||||
let menuRef = null;
|
||||
let buttonRef = null;
|
||||
|
||||
// generate unique ID for this dropdown instance
|
||||
const dropdownId = Symbol();
|
||||
|
||||
// subscribe to active dropdown store
|
||||
const unsubscribe = activeFormElement.subscribe((activeId) => {
|
||||
isMenuVisible = activeId === dropdownId;
|
||||
});
|
||||
|
||||
const toggle = (e) => {
|
||||
if (isMenuVisible) {
|
||||
activeFormElement.set(null);
|
||||
} else {
|
||||
document.addEventListener('click', handleClickWhenVisible);
|
||||
document.addEventListener('keydown', handleGlobalKeydown);
|
||||
activeFormElement.set(dropdownId); // set this as active, closing others
|
||||
|
||||
const viewportHeight = window.innerHeight;
|
||||
const menuHeight = 128; // max-h-32 in pixels
|
||||
const buffer = 20; // extra space to ensure some padding from viewport edges
|
||||
|
||||
let clickViewportY, pageX, pageY;
|
||||
|
||||
// Handle both mouse and keyboard events
|
||||
if (e.clientY !== undefined && e.pageX !== undefined) {
|
||||
// Mouse event
|
||||
clickViewportY = e.clientY;
|
||||
pageX = e.pageX;
|
||||
pageY = e.pageY;
|
||||
} else {
|
||||
// Keyboard event - use button position
|
||||
const buttonRect = buttonRef.getBoundingClientRect();
|
||||
clickViewportY = buttonRect.top;
|
||||
pageX = buttonRect.left + window.scrollX;
|
||||
pageY = buttonRect.top + window.scrollY;
|
||||
}
|
||||
|
||||
// is the room enough to show the box
|
||||
const shouldShowAbove = viewportHeight - clickViewportY < menuHeight + buffer;
|
||||
|
||||
// find position
|
||||
menuX = pageX - 192;
|
||||
menuY = shouldShowAbove
|
||||
? pageY - menuHeight // Position above click
|
||||
: pageY; // Position below click
|
||||
|
||||
menuRef.style = `left: ${menuX}px; top: ${menuY}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickWhenVisible = (event) => {
|
||||
if (isMenuVisible && menuRef && buttonRef) {
|
||||
activeFormElement.set(null);
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
document.removeEventListener('click', handleClickWhenVisible);
|
||||
};
|
||||
|
||||
const handleGlobalKeydown = (event) => {
|
||||
if (event.key === 'Escape' && isMenuVisible) {
|
||||
activeFormElement.set(null);
|
||||
document.removeEventListener('keydown', handleGlobalKeydown);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggle(e);
|
||||
} else if (e.key === 'Escape' && isMenuVisible) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
activeFormElement.set(null);
|
||||
}
|
||||
};
|
||||
|
||||
const _onDestroy = () => {
|
||||
document.removeEventListener('click', handleClickWhenVisible);
|
||||
document.removeEventListener('keydown', handleGlobalKeydown);
|
||||
unsubscribe();
|
||||
// Clear active dropdown if this one was active
|
||||
activeFormElement.update((current) => (current === dropdownId ? null : current));
|
||||
};
|
||||
|
||||
onDestroy(_onDestroy);
|
||||
|
||||
onMount(() => {
|
||||
return () => {
|
||||
_onDestroy();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="">
|
||||
<button
|
||||
bind:this={buttonRef}
|
||||
class="py-2 px-2"
|
||||
on:click|stopPropagation|preventDefault={toggle}
|
||||
on:keydown={handleKeydown}
|
||||
>
|
||||
<svg width="3.335557" height="16.465519" viewBox="0 0 0.88253281 4.3565019">
|
||||
<g transform="translate(-892.25669,88.863024)">
|
||||
<g transform="matrix(0,1.0139418,-1.0139418,0,802.48114,-807.2715)">
|
||||
<circle class="fill-cta-blue" cx="708.99603" cy="-88.976357" r="0.40846577" />
|
||||
<circle class="fill-cta-blue" cx="710.67859" cy="-88.976357" r="0.40846577" />
|
||||
<circle class="fill-cta-blue" cx="712.36115" cy="-88.976357" r="0.40846577" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
bind:this={menuRef}
|
||||
class="absolute bg-white drop-shadow-md z-20 max-h-32 w-48 rounded-md overflow-y-scroll {scrollBarClassesVertical}"
|
||||
class:hidden={!isMenuVisible}
|
||||
>
|
||||
<ul class="flex flex-col text-left">
|
||||
<slot />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<thead class="table-header-group">
|
||||
<slot />
|
||||
</thead>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import GhostText from '../GhostText.svelte';
|
||||
|
||||
export let column;
|
||||
/** @type {*|null} */
|
||||
export let pagination = null;
|
||||
export let alignText = 'left';
|
||||
export let sortable = false;
|
||||
export let size = '';
|
||||
// last tells if it the last field column before the actions column
|
||||
export let last = false;
|
||||
export let fillRest = false;
|
||||
export let isGhost = false;
|
||||
export let title = '';
|
||||
|
||||
let sortBy = pagination?.sortBy;
|
||||
let sortOrder = pagination?.sortOrder;
|
||||
const setSortAndSortBy = () => {
|
||||
if (!sortable || !pagination) {
|
||||
return;
|
||||
}
|
||||
pagination.sort(column.toLowerCase(), sortOrder.toLowerCase());
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (pagination) {
|
||||
pagination.onChange(() => {
|
||||
sortBy = pagination.sortBy;
|
||||
sortOrder = pagination.sortOrder;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<th
|
||||
class="pl-4 bg-grayblue-light py-4 border-hidden first:rounded-tl-lg first:rounded-bl-lg last:rounded-tr-lg last:border-4 min-w-48"
|
||||
class:rounded-br-lg={last}
|
||||
class:rounded-tr-lg={last}
|
||||
class:w-48={size === 'small'}
|
||||
class:w-56={size === 'medium'}
|
||||
class:w-80={size === 'large'}
|
||||
class:w-full={fillRest}
|
||||
>
|
||||
<button
|
||||
class="flex group cursor-pointer"
|
||||
class:pointer-events-none={!sortable}
|
||||
class:table-cell={alignText === 'center'}
|
||||
on:click|preventDefault={setSortAndSortBy}
|
||||
>
|
||||
<div class="w-full">
|
||||
<p class="font-bold text-slate-600 text-{alignText} flex">
|
||||
{#if !isGhost}
|
||||
{title.length ? title : column}
|
||||
{:else}
|
||||
<GhostText />
|
||||
{/if}
|
||||
{#if sortable && column.toLowerCase() === sortBy.toLowerCase() && !isGhost}
|
||||
<div
|
||||
class:bg-transparent={sortOrder === ''}
|
||||
class="flex justify-center items-center w-6 h-6 ml-2 rounded-md bg-cta-blue"
|
||||
>
|
||||
{#if sortOrder === 'asc'}
|
||||
<div>
|
||||
<img src="/arrow-up.svg" alt="arrow up" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if sortOrder === 'desc'}
|
||||
<div>
|
||||
<img src="/arrow-down.svg" alt="arrow down" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if sortOrder === ''}
|
||||
<p class="text-white"></p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</th>
|
||||
@@ -0,0 +1,5 @@
|
||||
<th class="bg-cta-light-blue w-20 py-4 border-hidden rounded-lg">
|
||||
<p class="font-bold text-slate-600 text-center">
|
||||
<slot />
|
||||
</p>
|
||||
</th>
|
||||
@@ -0,0 +1,5 @@
|
||||
<th class="pl-4 bg-grayblue-light w-48 py-4 border-hidden">
|
||||
<p class="font-bold text-slate-600 flex justify-center text-center">
|
||||
<slot />
|
||||
</p>
|
||||
</th>
|
||||
@@ -0,0 +1,3 @@
|
||||
<th class="pl-4 bg-white w-4 py-4 border-hidden">
|
||||
|
||||
</th>
|
||||
@@ -0,0 +1,5 @@
|
||||
<th class="pl-4 bg-grayblue-light w-48 py-4 border-hidden rounded-md">
|
||||
<p class="font-bold text-slate-600 text-left">
|
||||
<slot />
|
||||
</p>
|
||||
</th>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import TableHead from './TableHead.svelte';
|
||||
import TableHeadCell from './TableHeadCell.svelte';
|
||||
import TableHeadCellAction from './TableHeadCellAction.svelte';
|
||||
import TableHeadCellEmpty from './TableHeadCellEmpty.svelte';
|
||||
import TableRow from './TableRow.svelte';
|
||||
import TableRowEmpty from './TableRowEmpty.svelte';
|
||||
import GhostText from '../GhostText.svelte';
|
||||
|
||||
export let columns = [];
|
||||
export let sortable = [];
|
||||
export let isGhost = false;
|
||||
export let hasActions = true;
|
||||
/** @type {*|null} */
|
||||
export let pagination = null;
|
||||
|
||||
$: sortableMap = {};
|
||||
|
||||
onMount(() => {
|
||||
sortable.forEach((column) => {
|
||||
sortableMap[column.toLowerCase()] = true;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{#each columns as column, i (i)}
|
||||
{#if typeof column === 'object'}
|
||||
<TableHeadCell
|
||||
{...column}
|
||||
{...columns.length === 1 ? { size: '' } : {}}
|
||||
{pagination}
|
||||
{isGhost}
|
||||
sortable={sortableMap[column.column.toLowerCase()]}
|
||||
last={i === columns.length - 1}
|
||||
fillRest={i === columns.length - 1 && !hasActions}
|
||||
/>
|
||||
{:else}
|
||||
<TableHeadCell
|
||||
{column}
|
||||
{pagination}
|
||||
sortable={sortableMap[column.toLowerCase()]}
|
||||
last={i === columns.length - 1}
|
||||
fillRest={i === columns.length - 1 && !hasActions}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if hasActions}
|
||||
<TableHeadCellEmpty />
|
||||
<TableHeadCellAction>
|
||||
{#if !isGhost}
|
||||
Actions
|
||||
{:else}
|
||||
<GhostText center />
|
||||
{/if}
|
||||
</TableHeadCellAction>
|
||||
{/if}
|
||||
</TableRow>
|
||||
<TableRowEmpty />
|
||||
</TableHead>
|
||||
@@ -0,0 +1,3 @@
|
||||
<tr class="table-row odd:bg-white even:bg-pleasant-gray h-16 hover:bg-cta-light-blue">
|
||||
<slot />
|
||||
</tr>
|
||||
@@ -0,0 +1,3 @@
|
||||
<tr class="table-row bg-white h-4">
|
||||
<slot />
|
||||
</tr>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import TableDropDownButton from './TableDropDownButton.svelte';
|
||||
|
||||
export let disabled = false;
|
||||
export let title = '';
|
||||
export let name = 'Update';
|
||||
if (disabled && !title) {
|
||||
title = 'Disabled';
|
||||
}
|
||||
</script>
|
||||
|
||||
<TableDropDownButton {name} {title} on:click {disabled} />
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import TableDropDownButton from './TableDropDownButton.svelte';
|
||||
|
||||
export let disabled = false;
|
||||
export let title = '';
|
||||
export let name = 'View';
|
||||
if (disabled && !title) {
|
||||
title = 'Disabled';
|
||||
}
|
||||
</script>
|
||||
|
||||
<TableDropDownButton {name} {title} on:click {disabled} />
|
||||
Reference in new issue
Block a user