[READ-ONLY] Mirror of https://github.com/probablykasper/remind-me-again. Toggleable cron reminders app for Mac, Linux and Windows
linux macos notifications reminder tauri windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Create new reminders

Kasper (Jul 10, 2022, 6:10 AM +0200) 5fa1d669 49356dfb

+256 -48
+66 -4
src/App.svelte
··· 1 1 <script lang="ts"> 2 + import { cubicOut } from 'svelte/easing' 3 + import { crossfade } from 'svelte/transition' 4 + import { flip } from 'svelte/animate' 2 5 import './app.css' 3 6 import Item from './lib/Item.svelte' 7 + import New from './lib/New.svelte' 4 8 import type { Group } from './lib/types' 5 9 10 + let lastId = 0 11 + function newId() { 12 + return lastId++ 13 + } 6 14 let groups: Group[] = [ 7 - { title: 'Rabbit foot water', description: 'Yo', enabled: true, id: 1, nextDate: new Date() }, 8 - { title: 'Rabbit foot water', description: 'Yo', enabled: true, id: 0, nextDate: new Date() }, 9 - { title: 'Rabbit foot water', description: 'Yo', enabled: true, id: 2, nextDate: new Date() }, 15 + { 16 + title: 'Rabbit stuff', 17 + description: 'Yo', 18 + enabled: true, 19 + id: newId(), 20 + nextDate: new Date(), 21 + repeat: 'never', 22 + }, 23 + { 24 + title: 'Rabbit stuff', 25 + description: 'Yo', 26 + enabled: true, 27 + id: newId(), 28 + nextDate: new Date(), 29 + repeat: 'never', 30 + }, 31 + { 32 + title: 'Rabbit stuff', 33 + description: 'Yo', 34 + enabled: true, 35 + id: newId(), 36 + nextDate: new Date(), 37 + repeat: 'never', 38 + }, 10 39 ] 40 + 41 + const [send, receive] = crossfade({ 42 + duration: (d) => Math.sqrt(d * 200), 43 + fallback(node) { 44 + const style = getComputedStyle(node) 45 + const transform = style.transform === 'none' ? '' : style.transform 46 + 47 + return { 48 + easing: cubicOut, 49 + css: (t) => ` 50 + transform: ${transform} scale(${t}); 51 + opacity: ${t} 52 + `, 53 + } 54 + }, 55 + }) 11 56 </script> 12 57 13 58 <div class="flex min-h-screen w-full flex-col overflow-y-scroll px-4 py-2"> ··· 15 60 Reminders 16 61 </h1> 17 62 <div class="relative select-none outline-none"> 63 + <New 64 + onCreate={(group) => { 65 + group.id = newId() 66 + groups = [...groups, group] 67 + }} 68 + /> 18 69 {#each groups as group (group.id)} 19 - <Item bind:group /> 70 + <div 71 + in:receive={{ key: group.id, duration: 400 }} 72 + out:send={{ key: group.id, duration: 400 }} 73 + animate:flip={{ duration: 400 }} 74 + > 75 + <Item 76 + bind:group 77 + onDelete={() => { 78 + groups = groups.filter((g) => g.id !== group.id) 79 + }} 80 + /> 81 + </div> 20 82 {/each} 21 83 </div> 22 84 </div>
+40
src/lib/Edit.svelte
··· 1 + <script lang="ts"> 2 + import { DateInput } from 'date-picker-svelte' 3 + import { cubicOut } from 'svelte/easing' 4 + import { slide } from 'svelte/transition' 5 + import type { Group } from './types' 6 + 7 + export let group: Group 8 + export let onSave: () => void 9 + export let onCancel = () => { 10 + /* noop */ 11 + } 12 + </script> 13 + 14 + <div class="mt-2" transition:slide={{ easing: cubicOut }}> 15 + <DateInput 16 + bind:value={group.nextDate} 17 + closeOnSelection={true} 18 + --date-picker-background="#031212" 19 + --date-picker-foreground="#f7f7f7" 20 + --date-input-width="100%" 21 + --date-picker-highlight-border="hsl(183, 98%, 49%)" 22 + --date-picker-highlight-shadow="hsla(183, 98%, 49%, 50%)" 23 + --date-picker-selected-color="hsl(183, 100%, 85%)" 24 + --date-picker-selected-background="hsla(183, 98%, 49%, 20%)" 25 + /> 26 + </div> 27 + <div class="flex w-full"> 28 + <button 29 + type="button" 30 + class="mt-2 mr-2 w-full cursor-default rounded-sm bg-[#576f70] px-2 py-1 text-sm" 31 + transition:slide={{ easing: cubicOut }} 32 + on:click|stopPropagation={() => onCancel()}>Cancel</button 33 + > 34 + <button 35 + type="submit" 36 + class="mt-2 w-full cursor-default rounded-sm bg-[#31898c] px-2 py-1 text-sm" 37 + transition:slide={{ easing: cubicOut }} 38 + on:click|stopPropagation={() => onSave()}>Save</button 39 + > 40 + </div>
+51 -44
src/lib/Item.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte' 2 + import { onMount, tick } from 'svelte' 3 3 import type { Group } from './types' 4 - import { DateInput } from 'date-picker-svelte' 5 - import { slide } from 'svelte/transition' 6 - import { cubicOut } from 'svelte/easing' 7 4 import Switch from './Switch.svelte' 8 5 import ClickOutside from 'svelte-click-outside' 9 - import { checkShortcut } from './helpers' 6 + import { checkShortcut, invisibleCursorFix } from './helpers' 7 + import Edit from './Edit.svelte' 10 8 11 9 export let group: Group 10 + export let onDelete: () => void 12 11 let editMode = false 13 12 14 - let card: HTMLButtonElement 13 + let card: HTMLElement 15 14 let titleInput: HTMLInputElement 16 15 let textarea: HTMLTextAreaElement 17 16 onMount(resize) 18 17 function resize() { 19 - textarea.style.height = '' 20 - textarea.style.height = textarea.scrollHeight + 'px' 18 + if (textarea) { 19 + textarea.style.height = '' 20 + textarea.style.height = textarea.scrollHeight + 'px' 21 + } 21 22 } 22 23 function onInput() { 23 24 resize() 24 25 } 25 26 26 27 function onClickOutside() { 28 + cancel() 29 + } 30 + 31 + let originalGroup: string | null = null 32 + function startEdit() { 33 + if (!editMode) { 34 + editMode = true 35 + originalGroup = JSON.stringify(group) 36 + } 37 + } 38 + function cancel() { 27 39 editMode = false 40 + if (originalGroup) { 41 + group = JSON.parse(originalGroup) 42 + if (group.nextDate !== null) { 43 + group.nextDate = new Date(group.nextDate) 44 + } 45 + } 46 + } 47 + function save() { 48 + editMode = false 49 + card.focus() 28 50 } 29 51 30 - function keydown(e: KeyboardEvent) { 52 + async function keydown(e: KeyboardEvent) { 31 53 if (checkShortcut(e, 'Escape')) { 32 - editMode = false 33 - card.focus() 34 54 e.preventDefault() 55 + cancel() 56 + await tick() 57 + card.focus() 35 58 } 36 59 } 37 60 function keydownSelf(e: KeyboardEvent) { 38 61 if (checkShortcut(e, 'Enter')) { 39 - editMode = true 62 + startEdit() 40 63 titleInput.focus() 41 64 e.preventDefault() 65 + } else if (checkShortcut(e, 'Backspace')) { 66 + onDelete() 67 + e.preventDefault() 42 68 } 43 69 } 44 70 </script> 45 71 46 72 <ClickOutside on:clickoutside={onClickOutside}> 47 - <button 73 + <form 74 + on:submit|preventDefault={save} 48 75 bind:this={card} 49 - class="group my-3 flex w-full cursor-default items-center rounded-lg p-3.5 text-left shadow-xl outline-none transition-colors duration-150 ease-out hover:bg-[#133134] focus:ring-2 focus:ring-[#31898c]" 76 + class="group my-3 flex w-full cursor-default items-center rounded-lg p-3.5 text-left shadow-xl outline-none transition-colors duration-150 ease-out focus:bg-[#133134] active:bg-[#133134]" 50 77 class:bg-[#0E2426]={group.enabled} 51 - on:click={() => { 52 - editMode = true 53 - }} 78 + class:bg-[#133134]={editMode} 54 79 on:keydown={keydown} 80 + tabindex={editMode ? null : 0} 55 81 on:keydown|self={keydownSelf} 56 82 on:mousedown={(e) => { 57 83 if (editMode) { ··· 86 112 </div> 87 113 <div 88 114 class="mr-auto flex w-full flex-col" 89 - on:click={(e) => { 90 - if (editMode) { 91 - e.preventDefault() 92 - } 115 + on:click|preventDefault={() => { 116 + editMode = true 93 117 }} 94 118 > 95 119 <input ··· 97 121 class="w-full rounded-t-sm border-none bg-white bg-opacity-0 px-2 py-1 text-sm focus:ring-0" 98 122 class:bg-opacity-10={editMode} 99 123 tabindex={editMode ? 0 : -1} 124 + placeholder="Title" 100 125 type="text" 101 126 bind:value={group.title} 127 + use:invisibleCursorFix 102 128 /> 103 129 <textarea 104 130 bind:this={textarea} ··· 106 132 class="w-full resize-none rounded-b-sm border-none bg-white bg-opacity-0 px-2 py-1 text-xs text-white text-opacity-75 focus:ring-0" 107 133 class:bg-opacity-10={editMode} 108 134 tabindex={editMode ? 0 : -1} 135 + placeholder="Description" 109 136 type="text" 110 137 bind:value={group.description} 111 138 on:input={onInput} 112 139 /> 113 140 {#if editMode} 114 - <div class="mt-2" transition:slide={{ easing: cubicOut }}> 115 - <DateInput 116 - bind:value={group.nextDate} 117 - closeOnSelection={true} 118 - --date-picker-background="#031212" 119 - --date-picker-foreground="#f7f7f7" 120 - --date-input-width="100%" 121 - --date-picker-highlight-border="hsl(183, 98%, 49%)" 122 - --date-picker-highlight-shadow="hsla(183, 98%, 49%, 50%)" 123 - --date-picker-selected-color="hsl(183, 100%, 85%)" 124 - --date-picker-selected-background="hsla(183, 98%, 49%, 20%)" 125 - /> 126 - </div> 127 - <button 128 - class="mt-2 cursor-default rounded-sm bg-[#31898c] px-2 py-1 text-sm" 129 - transition:slide={{ easing: cubicOut }} 130 - on:click={(e) => { 131 - editMode = false 132 - e.stopPropagation() 133 - }}>Save</button 134 - > 141 + <Edit bind:group onSave={save} onCancel={cancel} /> 135 142 {/if} 136 143 </div> 137 144 <div on:click|preventDefault|stopPropagation> 138 145 <Switch class="ml-3.5" bind:value={group.enabled} /> 139 146 </div> 140 - </button> 147 + </form> 141 148 </ClickOutside> 142 149 143 150 <style lang="sass"> 144 151 // fix tailwind styles 145 152 :global(.date-time-field input) 146 - background-color: hsla(0, 0, 100, 0.1) 153 + background-color: hsla(0, 0%, 100%, 0.1) 147 154 border: none 148 155 font-size: 12px 149 156 :global(.date-time-picker select)
+78
src/lib/New.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte' 3 + import { cubicOut } from 'svelte/easing' 4 + import { slide } from 'svelte/transition' 5 + import Edit from './Edit.svelte' 6 + import type { Group } from './types' 7 + import { invisibleCursorFix } from './helpers' 8 + 9 + export let onCreate: (group: Group) => void 10 + let editMode = false 11 + 12 + let textarea: HTMLTextAreaElement 13 + onMount(resize) 14 + function resize() { 15 + if (textarea) { 16 + textarea.style.height = '' 17 + textarea.style.height = textarea.scrollHeight + 'px' 18 + } 19 + } 20 + function onInput() { 21 + resize() 22 + } 23 + 24 + let group: Group = newBlank() 25 + function newBlank(): Group { 26 + return { 27 + id: 0, 28 + enabled: true, 29 + title: '', 30 + description: '', 31 + nextDate: new Date(), 32 + repeat: 'never', 33 + } 34 + } 35 + $: editMode = group.title + group.description !== '' 36 + </script> 37 + 38 + <form 39 + class="flex w-full flex-col rounded bg-white bg-opacity-10" 40 + class:p-3.5={editMode} 41 + on:submit|preventDefault 42 + > 43 + <input 44 + class="w-full rounded-t-sm border-none bg-white bg-opacity-0 px-2 py-1 text-sm focus:ring-0" 45 + class:bg-opacity-10={editMode} 46 + placeholder={editMode ? 'Title' : 'New reminder...'} 47 + type="text" 48 + tabindex="0" 49 + bind:value={group.title} 50 + use:invisibleCursorFix 51 + /> 52 + {#if editMode} 53 + <textarea 54 + bind:this={textarea} 55 + rows="1" 56 + class="w-full resize-none rounded-b-sm border-none bg-white bg-opacity-0 px-2 py-1 text-xs text-white text-opacity-75 focus:ring-0" 57 + class:bg-opacity-10={editMode} 58 + placeholder="Description" 59 + tabindex={editMode ? 0 : -1} 60 + type="text" 61 + bind:value={group.description} 62 + on:input={onInput} 63 + transition:slide={{ easing: cubicOut }} 64 + /> 65 + <Edit 66 + bind:group 67 + onCancel={() => { 68 + editMode = false 69 + group = newBlank() 70 + }} 71 + onSave={() => { 72 + editMode = false 73 + onCreate(group) 74 + group = newBlank() 75 + }} 76 + /> 77 + {/if} 78 + </form>
+20
src/lib/helpers.ts
··· 32 32 if (e.key.toUpperCase() !== key.toUpperCase()) return false 33 33 return checkModifiers(e, options) 34 34 } 35 + 36 + /** 37 + * In Safari input elements, when you type something, delete it, tab and then shift+tab, the cursor is invisible. This fixes that 38 + */ 39 + export function invisibleCursorFix(node: HTMLInputElement) { 40 + const handleFocus = (e: FocusEvent) => { 41 + if (e.target instanceof HTMLInputElement) { 42 + // eslint-disable-next-line no-self-assign 43 + const value = e.target.value 44 + e.target.value = 'x' 45 + e.target.value = value 46 + } 47 + } 48 + node.addEventListener('focus', handleFocus) 49 + return { 50 + destroy() { 51 + node.removeEventListener('focus', handleFocus) 52 + }, 53 + } 54 + }
+1
src/lib/types.ts
··· 3 3 description: string 4 4 enabled: boolean 5 5 id: number 6 + repeat: 'never' | 'daily' 6 7 nextDate: Date | null 7 8 }