Skip to content

Commit

Permalink
feat(front): add drag and drop (colinlienard#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
colinlienard authored Jul 10, 2023
1 parent ac35f53 commit abd40d4
Show file tree
Hide file tree
Showing 13 changed files with 335 additions and 97 deletions.
31 changes: 27 additions & 4 deletions src/lib/components/common/IconButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,36 @@
import { createEventDispatcher } from 'svelte';
export let large = false;
export let rounded = false;
export let indicator = false;
const dispatch = createEventDispatcher();
</script>

<button class="button" class:large on:click={(event) => dispatch('click', event)}>
<button class="button" class:large class:rounded on:click={(event) => dispatch('click', event)}>
<slot />
{#if indicator}
<div class="indicator" />
{/if}
</button>

<style lang="scss">
.button {
padding: 0.5rem;
position: relative;
display: flex;
width: 2rem;
align-items: center;
justify-content: center;
border-radius: variables.$radius;
aspect-ratio: 1 / 1;
transition: background-color variables.$transition;
&:hover {
background-color: rgba(variables.$white, 0.03);
background-color: rgba(variables.$white, 0.04);
}
&:active {
background-color: rgba(variables.$white, 0.06);
background-color: rgba(variables.$white, 0.08);
}
:global(svg) {
Expand All @@ -31,5 +41,18 @@
&.large :global(svg) {
height: 1.25rem;
}
&.rounded {
border-radius: 50%;
}
.indicator {
position: absolute;
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
background-color: variables.$blue-2;
inset: 0 auto auto 0;
}
}
</style>
29 changes: 21 additions & 8 deletions src/lib/components/dashboard/Notification.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import type { NotificationData } from '~/lib/types';
export let data: NotificationData;
export let dragged = false;
export let interactive = true;
let {
Expand Down Expand Up @@ -91,9 +92,10 @@
}
</script>

<div class="container" class:transparent={!unread && !done}>
<div class="container" class:dragged>
<div
class="notification"
class:transparent={!unread && !done}
on:mouseenter={isNew && interactive ? () => (isNew = false) : undefined}
role="presentation"
>
Expand Down Expand Up @@ -144,7 +146,7 @@
{/each}
</ul>
{/if}
{#if interactive}
{#if !dragged && interactive}
<div class="over">
{#if !done}
{#if !unread && !pinned}
Expand Down Expand Up @@ -232,19 +234,21 @@

<style lang="scss">
.container {
border-radius: variables.$radius;
background-color: variables.$grey-1;
isolation: isolate;
&:not(:hover) {
.over {
opacity: 0;
}
}
&.transparent {
opacity: 0.65;
transition: opacity variables.$transition;
&.dragged {
@include mixins.modal-shadow;
&:hover {
opacity: 1;
}
rotate: -4deg;
transition: variables.$transition;
}
}
Expand All @@ -256,6 +260,15 @@
flex-direction: column;
padding: 1rem;
gap: 0.75rem;
&.transparent {
opacity: 0.65;
transition: opacity variables.$transition;
&:hover {
opacity: 1;
}
}
}
.new {
Expand Down
159 changes: 135 additions & 24 deletions src/lib/components/dashboard/NotificationColumn.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
<script lang="ts" context="module">
const dragging = writable<string | false>(false);
</script>

<script lang="ts">
import { onDestroy, onMount, type ComponentType } from 'svelte';
import { flip } from 'svelte/animate';
import { writable } from 'svelte/store';
import { fade, type CrossfadeParams, type TransitionConfig } from 'svelte/transition';
import { debounce } from '~/lib/helpers';
import { debounce, drag, drop, fetchGithub } from '~/lib/helpers';
import { ArrowUpIcon } from '~/lib/icons';
import { loading, largeScreen } from '~/lib/stores';
import {
loading,
largeScreen,
githubNotifications,
settings as settingsStore
} from '~/lib/stores';
import type { NotificationData } from '~/lib/types';
import Notification from './Notification.svelte';
import SkeletonEvent from './SkeletonEvent.svelte';
Expand All @@ -18,7 +28,7 @@
) => () => TransitionConfig;
export let icon: ComponentType;
export let title: string;
export let title: 'Pinned' | 'Unread' | 'Read';
export let notifications: NotificationData[];
export let placeholder: { icon: ComponentType; text: string };
export let transitions: {
Expand All @@ -31,7 +41,10 @@
let list: HTMLUListElement;
let scrolled = false;
let empty = !notifications.length;
let noScroll = false;
let dragId: string | null = null;
let scrollPosition = 0;
let transitioning = false;
let hovering = false;
const handleScroll = debounce((e: Event) => {
scrolled = (e.target as HTMLElement).scrollTop > 100;
Expand Down Expand Up @@ -60,18 +73,65 @@
$: {
notifications;
noScroll = true;
if ($dragging) {
transitioning = true;
setTimeout(() => {
transitioning = false;
}, settings.duration as number);
}
}
$: if ($dragging) {
scrollPosition = list?.scrollTop;
} else {
setTimeout(() => {
noScroll = false;
}, settings.duration as number);
scrollPosition = 0;
}, (settings.duration as number) + 10);
}
$: showDropzone = $dragging ? $dragging !== title : false;
function flipIfVisible(...args: Parameters<typeof flip>) {
const node = args[0].getBoundingClientRect();
const parent = args[0].parentElement?.getBoundingClientRect();
const isVisible = parent && node.top >= parent.top - 300 && node.bottom <= parent.bottom + 300;
return isVisible ? flip(...args) : { duration: 0, easing: () => 0 };
}
function conditionalFlip(...args: Parameters<typeof flip>) {
return args[2] ? flip(...args) : { duration: 0, easing: () => 0 };
function handleDragStart(id: string) {
dragId = id;
$dragging = title;
}
function handleDrop(id: string) {
dragId = null;
$dragging = false;
$githubNotifications = $githubNotifications.map((notification) => {
if (notification.id !== id) return notification;
if (title === 'Pinned') {
return {
...notification,
pinned: true,
unread: notification.unread || (!notification.pinned && $settingsStore.readWhenPin),
isNew: false
};
}
if (title === 'Read') {
return { ...notification, unread: false, pinned: false, isNew: false };
}
fetchGithub(`notifications/threads/${id}`, { method: 'PATCH' });
return { ...notification, unread: true, pinned: false, isNew: false };
});
}
</script>

<div class="column" class:vertical={$largeScreen}>
<div
class="column"
class:has-dropzone={showDropzone}
class:dragging={!!$dragging}
class:vertical={$largeScreen}
>
<div class="column-header">
<svelte:component this={icon} />
<h3 class="title">{title}</h3>
Expand All @@ -80,26 +140,42 @@
<slot name="header-addon" />
</div>
</div>
{#if showDropzone}
<div class="dropzone" class:hovering transition:fade={{ duration: 150 }} />
{/if}
{#if scrolled}
<div class="scroll-button" transition:fade={{ duration: 150 }}>
<Button type="secondary" small on:click={handleScrollToTop}>
Scroll to top <ArrowUpIcon />
</Button>
</div>
{/if}
<ul class="list" class:no-scroll={noScroll || empty || !$largeScreen} bind:this={list}>
<ul
class="list"
class:scroll-visible={transitioning || empty || !$largeScreen || !!$dragging}
style="--scroll-position: -{scrollPosition}px"
use:drop={{
onDrop: handleDrop,
onHoverChange: (value) => (hovering = value)
}}
bind:this={list}
>
{#if $loading}
<li><SkeletonEvent /></li>
<li><SkeletonEvent /></li>
{:else}
{#each notifications as notification, index (notification)}
{#each notifications as notification (notification)}
<li
class="item"
in:receive={{ key: notification.id }}
out:send={{ key: notification.id }}
animate:conditionalFlip={index < 6 ? settings : undefined}
animate:flipIfVisible={settings}
use:drag={{
id: notification.id,
onDragStart: handleDragStart
}}
>
<Notification data={notification} />
<Notification data={notification} dragged={!!$dragging && notification.id === dragId} />
</li>
{/each}
{#if empty}
Expand All @@ -124,6 +200,15 @@
flex-direction: column;
padding: 0 0.5rem 0 1.5rem;
&.has-dropzone {
z-index: -1;
}
&:not(.has-dropzone) {
z-index: 10;
transition: z-index 0.3s;
}
&.vertical {
min-height: 0;
}
Expand All @@ -143,13 +228,30 @@
}
}
&::before {
position: absolute;
z-index: 1;
height: 5.5rem;
background-image: linear-gradient(variables.$grey-1 4.5rem, transparent);
content: '';
inset: -3rem 0 auto;
}
&::after {
position: absolute;
z-index: 1;
background-image: linear-gradient(transparent, variables.$grey-1 1rem);
content: '';
inset: calc(100% - 1rem) 0 -2rem 0;
}
&.dragging {
&::before,
&::after,
.column-header {
z-index: -1;
}
}
}
.column-header {
Expand All @@ -159,15 +261,6 @@
margin-right: 1rem;
gap: 0.5rem;
&::before {
position: absolute;
z-index: 1;
height: 3.5rem;
background-image: linear-gradient(variables.$grey-1 2.5rem, transparent);
content: '';
inset: -1rem 0 auto;
}
:global(svg) {
z-index: 2;
height: 1.25rem;
Expand All @@ -190,6 +283,23 @@
}
}
.dropzone {
position: absolute;
z-index: 2;
border-radius: variables.$radius;
background-color: rgba(variables.$blue-2, 0.1);
content: '';
inset: 2.25rem 1.5rem 0;
opacity: 0.5;
outline: 6px dashed variables.$blue-3;
pointer-events: none;
transition: opacity variables.$transition;
&.hovering {
opacity: 1;
}
}
.scroll-button {
position: absolute;
z-index: 10;
Expand All @@ -205,8 +315,9 @@
padding: 1rem 1rem 1rem 0;
gap: 1rem;
&.no-scroll {
&.scroll-visible {
overflow: visible;
transform: translateY(var(--scroll-position));
}
&::-webkit-scrollbar {
Expand Down
Loading

0 comments on commit abd40d4

Please sign in to comment.