mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-06-13 09:47:51 +02:00
improve campaign ui
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
<script>
|
||||
export let variant = 'blue'; // blue, green, orange, red, gray
|
||||
export let icon = 'edit'; // edit, close, anonymize, export, upload, delete, copy, view
|
||||
export let disabled = false;
|
||||
export let type = 'button';
|
||||
|
||||
// color mappings for variants
|
||||
const variantClasses = {
|
||||
blue: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500',
|
||||
green: 'bg-green-700 hover:bg-green-800 focus:ring-green-600',
|
||||
orange: 'bg-orange-600 hover:bg-orange-700 focus:ring-orange-500',
|
||||
red: 'bg-red-600 hover:bg-red-700 focus:ring-red-500',
|
||||
gray: 'bg-gray-600 hover:bg-gray-700 focus:ring-gray-500'
|
||||
};
|
||||
|
||||
$: colorClass = variantClasses[variant] || variantClasses.blue;
|
||||
</script>
|
||||
|
||||
<button
|
||||
{type}
|
||||
{disabled}
|
||||
on:click
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white {colorClass} focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{#if icon === 'edit'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<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>
|
||||
{:else if icon === 'close'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if icon === 'anonymize'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
/>
|
||||
</svg>
|
||||
{:else if icon === 'export'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if icon === 'upload'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
{:else if icon === 'delete'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
{:else if icon === 'copy'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if icon === 'view'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
<slot />
|
||||
</button>
|
||||
@@ -44,6 +44,7 @@
|
||||
import { globalButtonDisabledAttributes } from '$lib/utils/form';
|
||||
import FileField from '$lib/components/FileField.svelte';
|
||||
import ConditionalDisplay from '$lib/components/ConditionalDisplay.svelte';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
|
||||
// services
|
||||
const appStateService = AppStateService.instance;
|
||||
@@ -1210,61 +1211,88 @@
|
||||
<EventTimeline events={timelineEvents} isGhost={isTimelineGhost} />
|
||||
</div>
|
||||
|
||||
<div class=" space-y-6">
|
||||
<!-- First Row: Details and Timeline -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2">
|
||||
<!-- Primary Campaign Info -->
|
||||
<div class="py-6 rounded-lg">
|
||||
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
|
||||
Details
|
||||
</h3>
|
||||
<div class="grid grid-cols-[120px_1fr] gap-y-3">
|
||||
<span class="text-grayblue-dark font-medium">Status:</span>
|
||||
<!-- details and actions section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<!-- campaign details card -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-900/80 p-6 rounded-lg shadow-md dark:shadow-none transition-all duration-200 dark:ring-1 dark:ring-gray-600/30"
|
||||
>
|
||||
<h3
|
||||
class="text-lg font-semibold text-pc-darkblue dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
Campaign Details
|
||||
</h3>
|
||||
<div class="space-y-2.5 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Status:</span>
|
||||
<span class="text-pc-darkblue dark:text-white font-semibold">
|
||||
{toEvent(campaign.notableEventName).name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Template:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
<button
|
||||
class="cursor-pointer text-cta-blue dark:text-white underline hover:opacity-75"
|
||||
on:click={() => {
|
||||
openTemplateModal(campaign.template?.id);
|
||||
}}
|
||||
>
|
||||
{campaign.template?.name}
|
||||
</button>
|
||||
</span>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Template:</span>
|
||||
<button
|
||||
class="text-cta-blue dark:text-blue-400 hover:underline text-right"
|
||||
on:click={() => {
|
||||
openTemplateModal(campaign.template?.id);
|
||||
}}
|
||||
>
|
||||
{campaign.template?.name}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Groups:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Groups:</span>
|
||||
<div class="text-right">
|
||||
{#each campaign.recipientGroups as group, i}
|
||||
<a
|
||||
class="hover:underline text-cta-blue dark:text-white hover:opacity-75 underline"
|
||||
class="text-cta-blue dark:text-blue-400 hover:underline"
|
||||
href="/recipient/group/{recipientGroupMap.byValue(group)}"
|
||||
target="_blank">{group}</a
|
||||
>{#if i !== (campaign.recipientGroups?.length ?? 0) - 1}
|
||||
,
|
||||
{/if}
|
||||
>{#if i !== (campaign.recipientGroups?.length ?? 0) - 1}, {/if}
|
||||
{/each}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Type:</span>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Type:</span>
|
||||
<span class="text-pc-darkblue dark:text-white"
|
||||
>{isSelfManaged ? 'Self Managed' : 'Scheduled'}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<ConditionalDisplay show="blackbox">
|
||||
<span class="text-grayblue-dark font-medium">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Webhook:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{campaign.webhookID ? 'Yes' : 'None'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Data Saving:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{campaign.saveSubmittedData ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Test:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">{campaign.isTest ? 'Yes' : 'No'}</span>
|
||||
</div>
|
||||
|
||||
<ConditionalDisplay show="blackbox">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">
|
||||
{campaign?.allowDeny ? (allowedFilter ? 'Allow' : 'Deny') : ''}
|
||||
IP Filters:
|
||||
</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
<div class="text-right">
|
||||
{#if campaign.allowDeny?.length}
|
||||
{#each campaign.allowDeny as allowDeny, i}
|
||||
<a
|
||||
href="/filter/?edit={allowDeny.id}"
|
||||
class="text-cta-blue dark:text-white hover:underline"
|
||||
class="text-cta-blue dark:text-blue-400 hover:underline"
|
||||
target="_blank"
|
||||
>
|
||||
{allowDeny.name}
|
||||
@@ -1273,238 +1301,216 @@
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
None
|
||||
<span class="text-pc-darkblue dark:text-white">None</span>
|
||||
{/if}
|
||||
</span>
|
||||
</ConditionalDisplay>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConditionalDisplay show="blackbox">
|
||||
<span class="text-grayblue-dark font-medium">Deny Page:</span>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Deny Page:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{#if campaign.denyPage}
|
||||
{campaign.denyPage.name}
|
||||
{:else}
|
||||
None
|
||||
{/if}
|
||||
{campaign.denyPage ? campaign.denyPage.name : 'None'}
|
||||
</span>
|
||||
</ConditionalDisplay>
|
||||
</div>
|
||||
|
||||
<ConditionalDisplay show="blackbox">
|
||||
<span class="text-grayblue-dark font-medium">Evasion Page:</span>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Evasion Page:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{#if campaign.evasionPage}
|
||||
{campaign.evasionPage.name}
|
||||
{:else}
|
||||
None
|
||||
{/if}
|
||||
{campaign.evasionPage ? campaign.evasionPage.name : 'None'}
|
||||
</span>
|
||||
</ConditionalDisplay>
|
||||
</div>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Webhook:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{#if campaign.webhookID}
|
||||
Yes
|
||||
{:else}
|
||||
None
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Data Saving:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{campaign.saveSubmittedData ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
|
||||
<ConditionalDisplay show="blackbox">
|
||||
<span class="text-grayblue-dark font-medium">Metadata:</span>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Metadata:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{campaign.saveBrowserMetadata ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</ConditionalDisplay>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Test:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">{campaign.isTest ? 'Yes' : 'No'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ConditionalDisplay>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 rounded-lg">
|
||||
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
|
||||
Timeline
|
||||
</h3>
|
||||
<div class="grid grid-cols-[120px_1fr] gap-y-3">
|
||||
<span class="text-grayblue-dark font-medium">Created:</span>
|
||||
<span class="text-pc-darkblue dark:text-white"
|
||||
<!-- timeline card -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-900/80 p-6 rounded-lg shadow-md dark:shadow-none transition-all duration-200 dark:ring-1 dark:ring-gray-600/30"
|
||||
>
|
||||
<h3
|
||||
class="text-lg font-semibold text-pc-darkblue dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
Timeline
|
||||
</h3>
|
||||
<div class="space-y-2.5 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Created:</span>
|
||||
<span class="text-pc-darkblue dark:text-white text-right"
|
||||
><Datetime value={campaign.createdAt} /></span
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if !isSelfManaged}
|
||||
<span class="text-grayblue-dark font-medium">Delivery start:</span>
|
||||
<span class="text-pc-darkblue dark:text-white"
|
||||
{#if !isSelfManaged}
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Delivery start:</span>
|
||||
<span class="text-pc-darkblue dark:text-white text-right"
|
||||
><Datetime value={campaign.sendStartAt} /></span
|
||||
>
|
||||
</div>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Delivery finish:</span>
|
||||
<span class="text-pc-darkblue dark:text-white"
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Delivery finish:</span>
|
||||
<span class="text-pc-darkblue dark:text-white text-right"
|
||||
><Datetime value={campaign.sendEndAt} /></span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Close At:</span>
|
||||
<span class="text-pc-darkblue dark:text-white"
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Close At:</span>
|
||||
<span class="text-pc-darkblue dark:text-white text-right"
|
||||
><Datetime value={campaign.closeAt} /></span
|
||||
>
|
||||
</div>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Closed:</span>
|
||||
<span class="text-pc-darkblue dark:text-white"
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Closed:</span>
|
||||
<span class="text-pc-darkblue dark:text-white text-right"
|
||||
><Datetime value={campaign.closedAt} /></span
|
||||
>
|
||||
</div>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Anonymize At:</span>
|
||||
<span class="text-pc-darkblue dark:text-white"
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Anonymize At:</span>
|
||||
<span class="text-pc-darkblue dark:text-white text-right"
|
||||
><Datetime value={campaign.anonymizeAt} /></span
|
||||
>
|
||||
</div>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Anonymized:</span>
|
||||
<span class="text-pc-darkblue dark:text-white"
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Anonymized:</span>
|
||||
<span class="text-pc-darkblue dark:text-white text-right"
|
||||
><Datetime value={campaign.anonymizedAt} /></span
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if campaign.constraintWeekDays}
|
||||
<div class="py-6 rounded-lg">
|
||||
<div class="flex gap-8">
|
||||
<!-- Days Schedule -->
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-grayblue-dark font-medium">Schedule:</span>
|
||||
</div>
|
||||
<div class="flex gap-1.5">
|
||||
{#each formatWeekDays(campaign.constraintWeekDays).days as day}
|
||||
<div class="relative flex flex-col items-center">
|
||||
<div
|
||||
class="w-10 h-10 flex items-center justify-center rounded-lg transition-all {day.isActive
|
||||
? 'bg-cta-blue text-white font-medium shadow-sm hover:bg-cta-blue'
|
||||
: 'bg-slate-50 text-slate-500 border border-slate-200 hover:bg-slate-100'}"
|
||||
title={day.full}
|
||||
>
|
||||
{day.short}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if formatWeekDays(campaign.constraintWeekDays).summary}
|
||||
<div class="text-sm text-slate-500 mt-2">
|
||||
{formatWeekDays(campaign.constraintWeekDays).summary}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Time Schedule -->
|
||||
{#if campaign.constraintStartTime && campaign.constraintEndTime}
|
||||
<div class="flex-1">
|
||||
<div class="mb-3">
|
||||
<span class="text-grayblue-dark font-medium"></span>
|
||||
<button
|
||||
on:click={() => timeFormat.update((f) => !f)}
|
||||
class="text-xs px-2 py-1 rounded border justify-self-start border-slate-200 hover:bg-slate-50 text-slate-600"
|
||||
>
|
||||
{$timeFormat ? '12h' : '24h'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="bg-cta-blue text-white w-16 text-center h-9 py-1 px-2 rounded-lg shadow-sm font-medium"
|
||||
class:w-24={!$timeFormat}
|
||||
>
|
||||
{formatTimeConstraint(campaign.constraintStartTime, $timeFormat)}
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-slate-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="h-9 bg-cta-blue text-white text-center w-16 py-1 px-2 rounded-lg shadow-sm font-medium"
|
||||
class:w-24={!$timeFormat}
|
||||
>
|
||||
{formatTimeConstraint(campaign.constraintEndTime, $timeFormat)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div> </div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Second Row: Schedule Constraints and Actions -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div></div>
|
||||
<div class="p-6 rounded-lg">
|
||||
<h3 class="text-xl font-semibold text-pc-darkblue dark:text-white mb-4 border-b pb-2">
|
||||
Actions
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{#if !campaignUpdateDisabledAndTitle(campaign).disabled}
|
||||
<button
|
||||
on:click={onClickUpdateCampaign}
|
||||
class="px-4 py-2 bg-gradient-to-r from-blue-500 to-blue-600 hover:opacity-80 text-white rounded-md transition-colors"
|
||||
|
||||
{#if campaign.constraintWeekDays}
|
||||
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="mb-3">
|
||||
<span class="text-gray-600 dark:text-gray-400 text-sm font-medium">Schedule:</span>
|
||||
</div>
|
||||
<div class="flex gap-1.5 mb-3">
|
||||
{#each formatWeekDays(campaign.constraintWeekDays).days as day}
|
||||
<div
|
||||
class="w-8 h-8 flex items-center justify-center rounded text-xs transition-all {day.isActive
|
||||
? 'bg-cta-blue text-white font-medium'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 border border-gray-200 dark:border-gray-700'}"
|
||||
title={day.full}
|
||||
>
|
||||
Update
|
||||
{day.short}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if campaign.constraintStartTime && campaign.constraintEndTime}
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<span
|
||||
class="bg-cta-blue text-white px-2 py-1 rounded text-xs font-medium"
|
||||
class:px-3={!$timeFormat}
|
||||
>
|
||||
{formatTimeConstraint(campaign.constraintStartTime, $timeFormat)}
|
||||
</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-gray-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="bg-cta-blue text-white px-2 py-1 rounded text-xs font-medium"
|
||||
class:px-3={!$timeFormat}
|
||||
>
|
||||
{formatTimeConstraint(campaign.constraintEndTime, $timeFormat)}
|
||||
</span>
|
||||
<button
|
||||
on:click={() => timeFormat.update((f) => !f)}
|
||||
class="ml-auto text-xs px-2 py-1 rounded border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{$timeFormat ? '12h' : '24h'}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- actions card -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-900/80 p-6 rounded-lg shadow-md dark:shadow-none transition-all duration-200 dark:ring-1 dark:ring-gray-600/30"
|
||||
>
|
||||
<h3
|
||||
class="text-lg font-semibold text-pc-darkblue dark:text-white mb-4 pb-2 border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
Actions
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<!-- management actions -->
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1 uppercase tracking-wide">
|
||||
Manage
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#if !campaignUpdateDisabledAndTitle(campaign).disabled}
|
||||
<IconButton variant="blue" icon="edit" on:click={onClickUpdateCampaign}>
|
||||
Update
|
||||
</IconButton>
|
||||
{/if}
|
||||
<button
|
||||
on:click={showCloseCampaignModal}
|
||||
<IconButton
|
||||
variant="orange"
|
||||
icon="close"
|
||||
disabled={!!campaign.closedAt}
|
||||
class="px-4 py-2 bg-gradient-to-r from-pc-red to-repeart-submissions hover:opacity-80 text-white rounded-md disabled:opacity-10 disabled:cursor-not-allowed transition-colors"
|
||||
on:click={showCloseCampaignModal}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
on:click={showAnonymizeModal}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="red"
|
||||
icon="anonymize"
|
||||
disabled={!!campaign.anonymizedAt}
|
||||
class="px-4 py-2 bg-gradient-to-r from-pc-red to-repeart-submissions hover:opacity-80 text-white rounded-md disabled:opacity-10 disabled:cursor-not-allowed transition-colors"
|
||||
on:click={showAnonymizeModal}
|
||||
>
|
||||
Anonymize
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Buttons -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
on:click={onClickExportEvents}
|
||||
class="px-4 py-2 bg-gradient-to-r from-campaign-active to-campaign-scheduled hover:opacity-80 text-white rounded-md disabled:opacity-10 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Export events
|
||||
</button>
|
||||
<button
|
||||
on:click={onClickExportSubmissions}
|
||||
class="px-4 py-2 bg-gradient-to-r from-campaign-active to-campaign-scheduled hover:opacity-80 text-white rounded-md disabled:opacity-10 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Export submitters
|
||||
</button>
|
||||
<!-- export actions -->
|
||||
<div class="pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1 uppercase tracking-wide">
|
||||
Export
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<IconButton variant="green" icon="export" on:click={onClickExportEvents}>
|
||||
Export Events
|
||||
</IconButton>
|
||||
<IconButton variant="green" icon="export" on:click={onClickExportSubmissions}>
|
||||
Export Submitters
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Section -->
|
||||
<div class="border-t pt-4">
|
||||
<div>
|
||||
<FileField accept=".csv" on:change={onUploadReportedCSV}>
|
||||
Upload Reported CSV
|
||||
</FileField>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
CSV format: "Reported by" (email), "Date reported(UTC+02:00)"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- import section -->
|
||||
<div class="pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1 uppercase tracking-wide">
|
||||
Import
|
||||
</p>
|
||||
<FileField accept=".csv" on:change={onUploadReportedCSV}>Reported CSV</FileField>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Format: "Reported by" (email), "Date reported(UTC+02:00)"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user