diff --git a/Just/Just.csproj b/Just/Just.csproj index 62615a9..db7e65b 100644 --- a/Just/Just.csproj +++ b/Just/Just.csproj @@ -4,7 +4,6 @@ - \ No newline at end of file diff --git a/Just/index.html b/Just/index.html index 560f4aa..b16afaa 100644 --- a/Just/index.html +++ b/Just/index.html @@ -10,4 +10,4 @@
- + \ No newline at end of file diff --git a/Just/src/App.vue b/Just/src/App.vue index e522b9a..bc2a7a0 100644 --- a/Just/src/App.vue +++ b/Just/src/App.vue @@ -1,47 +1,99 @@ \ No newline at end of file diff --git a/Just/src/components/_ui-dropdown-button.vue b/Just/src/components/_ui-dropdown-button.vue new file mode 100644 index 0000000..30c7da7 --- /dev/null +++ b/Just/src/components/_ui-dropdown-button.vue @@ -0,0 +1,289 @@ + + + + + + + \ No newline at end of file diff --git a/Just/src/components/_ui-dropdown.vue b/Just/src/components/_ui-dropdown.vue new file mode 100644 index 0000000..48a9aff --- /dev/null +++ b/Just/src/components/_ui-dropdown.vue @@ -0,0 +1,238 @@ + + + + + + + \ No newline at end of file diff --git a/Just/src/components/index.ts b/Just/src/components/index.ts index b500a02..299b8d5 100644 --- a/Just/src/components/index.ts +++ b/Just/src/components/index.ts @@ -1,7 +1,11 @@ import { type App } from 'vue'; import uiIcon from './ui-icon.vue'; +import uiContextmenu from './ui-contextmenu.vue'; +import uiContextmenuButton from './ui-contextmenu-button.vue'; export function createComponents (app: App) { app.component('ui-icon', uiIcon); + app.component('ui-contextmenu', uiContextmenu); + app.component('ui-contextmenu-button', uiContextmenuButton); } \ No newline at end of file diff --git a/Just/src/components/ui-contextmenu-button.vue b/Just/src/components/ui-contextmenu-button.vue new file mode 100644 index 0000000..5b4afbe --- /dev/null +++ b/Just/src/components/ui-contextmenu-button.vue @@ -0,0 +1,74 @@ + + + + + + \ No newline at end of file diff --git a/Just/src/components/ui-contextmenu.vue b/Just/src/components/ui-contextmenu.vue new file mode 100644 index 0000000..dabb7a9 --- /dev/null +++ b/Just/src/components/ui-contextmenu.vue @@ -0,0 +1,141 @@ + + + + + + \ No newline at end of file diff --git a/Just/src/db/index.ts b/Just/src/db/index.ts deleted file mode 100644 index b759a91..0000000 --- a/Just/src/db/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { type Post } from '@/models'; -import createCollection from '@/db/createCollection.ts' - -export const posts = await createCollection('posts'); \ No newline at end of file diff --git a/Just/src/locales/en.json b/Just/src/locales/en.json index ee4d1fb..0283c20 100644 --- a/Just/src/locales/en.json +++ b/Just/src/locales/en.json @@ -1,3 +1,5 @@ { - "app.name": "Just" + "app.name": "Just", + "menu.lists.add": "Add a list", + "menu.settings": "Settings" } \ No newline at end of file diff --git a/Just/src/main.ts b/Just/src/main.ts index e8f18ee..2dddbda 100644 --- a/Just/src/main.ts +++ b/Just/src/main.ts @@ -2,6 +2,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import { createI18n } from 'vue-i18n' import { createComponents } from '@/components' +import { createDirectives } from '@/utils/directives' import './styles/styles.scss' import App from './App.vue' @@ -10,8 +11,10 @@ import en_translations from './locales/en.json' const app = createApp(App) createComponents(app) +createDirectives(app) app.use(createPinia()) app.use(createI18n<[typeof en_translations], 'en'>({ + legacy: false, locale: 'en', messages: { en: en_translations diff --git a/Just/src/models/entity.ts b/Just/src/models/entity.ts new file mode 100644 index 0000000..f194852 --- /dev/null +++ b/Just/src/models/entity.ts @@ -0,0 +1,7 @@ + +export type Entity = { + id: string, + createdAt: number, + lastModifiedAt: number, + sort: number, +}; \ No newline at end of file diff --git a/Just/src/models/index.ts b/Just/src/models/index.ts index aa219ef..7955355 100644 --- a/Just/src/models/index.ts +++ b/Just/src/models/index.ts @@ -1 +1,3 @@ -export { type Post } from './post' \ No newline at end of file +export { type Entity } from './entity' +export { type Task } from './task' +export { type TaskList } from './taskList' \ No newline at end of file diff --git a/Just/src/models/task.ts b/Just/src/models/task.ts new file mode 100644 index 0000000..5939200 --- /dev/null +++ b/Just/src/models/task.ts @@ -0,0 +1,9 @@ +import { type Entity } from './entity'; + +export type Task = { + name: string, + done: boolean, + listId: string, + dueAt?: Date, + remindAt?: Date +} & Entity; \ No newline at end of file diff --git a/Just/src/models/taskList.ts b/Just/src/models/taskList.ts new file mode 100644 index 0000000..fd63fb7 --- /dev/null +++ b/Just/src/models/taskList.ts @@ -0,0 +1,5 @@ +import { type Entity } from './entity'; + +export type TaskList = { + name: string +} & Entity; \ No newline at end of file diff --git a/Just/src/components/__tests__/HelloWorld.spec.ts b/Just/src/modules/__tests__/HelloWorld.spec.ts similarity index 100% rename from Just/src/components/__tests__/HelloWorld.spec.ts rename to Just/src/modules/__tests__/HelloWorld.spec.ts diff --git a/Just/src/modules/app-menu.vue b/Just/src/modules/app-menu.vue new file mode 100644 index 0000000..83087f3 --- /dev/null +++ b/Just/src/modules/app-menu.vue @@ -0,0 +1,95 @@ + + + + + \ No newline at end of file diff --git a/Just/src/components/navigation-item.vue b/Just/src/modules/navigation-item.vue similarity index 74% rename from Just/src/components/navigation-item.vue rename to Just/src/modules/navigation-item.vue index dfea7a0..f7bfd11 100644 --- a/Just/src/components/navigation-item.vue +++ b/Just/src/modules/navigation-item.vue @@ -1,28 +1,43 @@ \ No newline at end of file diff --git a/Just/src/db/createCollection.ts b/Just/src/stores/createCollection.ts similarity index 100% rename from Just/src/db/createCollection.ts rename to Just/src/stores/createCollection.ts diff --git a/Just/src/db/createIndexedDBAdapter.ts b/Just/src/stores/createIndexedDBAdapter.ts similarity index 100% rename from Just/src/db/createIndexedDBAdapter.ts rename to Just/src/stores/createIndexedDBAdapter.ts diff --git a/Just/src/stores/index.ts b/Just/src/stores/index.ts new file mode 100644 index 0000000..6dff16c --- /dev/null +++ b/Just/src/stores/index.ts @@ -0,0 +1,8 @@ +import { type Task, type TaskList } from '@/models'; +import createCollection from './createCollection.ts' + +export const taskListsCollection = await createCollection('taskLists'); +export const tasksCollection = await createCollection('tasks'); + +export * as tasks from './tasks.ts'; +export * as taskLists from './lists.ts'; \ No newline at end of file diff --git a/Just/src/stores/lists.ts b/Just/src/stores/lists.ts new file mode 100644 index 0000000..635f275 --- /dev/null +++ b/Just/src/stores/lists.ts @@ -0,0 +1,91 @@ +import { taskListsCollection, tasksCollection } from '.' +import { type TaskList } from '@/models' + +/** + * Stores a new task list + * @param name The name of the list + * @returns The newly created task list + */ +export async function add(name: string): Promise +{ + const modelBefore: TaskList | undefined = await taskListsCollection.find(undefined, { + limit: 1, + sort: { + sort: -1 + }, + }).fetch().pop(); + + const sort: number = (modelBefore?.sort ?? 0) + 1; + const date: number = Date.now(); + + return await taskListsCollection.insert({ + name, + sort, + createdAt: date, + lastModifiedAt: date + }); +} + + +/** + * Stores updated properties of a list + * (only name + sort get updated) + * @param model The existing list + * @returns The updated task list + */ +export async function update(model: TaskList): Promise +{ + const lastModifiedAt = Date.now(); + + let updated: boolean = (await taskListsCollection.updateOne({ id: model.id }, { + $set: { + lastModifiedAt, + name: model.name, + sort: model.sort + } + })) === 1; + + if (updated) + { + model.lastModifiedAt = lastModifiedAt; + } + + return model; +} + + +/** + * Try to get a single task list from the store + * @param id Id of the list + * @returns The task list, if found + */ +export async function get(id: string): Promise +{ + return await taskListsCollection.findOne({ id }); +} + + +/** + * Get all task lists from the store + * @returns Alle stored task lists + */ +export async function getAll(): Promise +{ + return await taskListsCollection.find(undefined, { + sort: { + sort: 1 + } + }).fetch(); +} + + +/** + * Tries to remove a task list by id. + * This also removes all associated tasks. + * @param id Id of the list + */ +export async function remove(id: string): Promise +{ + await taskListsCollection.removeOne({ id }); + await tasksCollection.removeMany({ listId: id }); +} \ No newline at end of file diff --git a/Just/src/stores/tasks.ts b/Just/src/stores/tasks.ts new file mode 100644 index 0000000..e69de29 diff --git a/Just/src/styles/config.scss b/Just/src/styles/config.scss new file mode 100644 index 0000000..60d631a --- /dev/null +++ b/Just/src/styles/config.scss @@ -0,0 +1,10 @@ + +:root +{ + --space-s: 8px; + --space-m: 12px; + --space: 16px; + + --radius: 8px; + --radius-inner: 6px; +} \ No newline at end of file diff --git a/Just/src/styles/main.scss b/Just/src/styles/main.scss index e72a303..05947c5 100644 --- a/Just/src/styles/main.scss +++ b/Just/src/styles/main.scss @@ -7,13 +7,38 @@ src: url('./inter.woff2') format("woff2"); } +html, body +{ + font-size: 14px; +} + body { color: var(--color-text); font-family: 'inter', sans-serif; - font-size: 14px; line-height: 1.5; background: var(--color-layout-bg); + + &.is-resizing * + { + transition: none !important; + animation-play-state: paused !important; + } +} + +.app +{ + width: min(800px, 100%); + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 30px; + padding: 10px; + height: 100vh; + + @media (max-width: 700px) + { + display: block; + } } .app-icon @@ -22,4 +47,13 @@ body stroke-linecap: round; stroke-linejoin: round; fill: transparent; +} + +button +{ + appearance: none; + background: none; + border: none; + color: var(--color-text); + cursor: pointer; } \ No newline at end of file diff --git a/Just/src/styles/styles.scss b/Just/src/styles/styles.scss index ddeb858..8b3a2ce 100644 --- a/Just/src/styles/styles.scss +++ b/Just/src/styles/styles.scss @@ -1,4 +1,5 @@ +@use './config'; @use './theme'; @use './reset'; @use './main'; \ No newline at end of file diff --git a/Just/src/styles/theme.scss b/Just/src/styles/theme.scss index 523e8a9..280954c 100644 --- a/Just/src/styles/theme.scss +++ b/Just/src/styles/theme.scss @@ -6,7 +6,7 @@ --color-bg-shade-2: #1D1D1D; --color-bg-shade-3: #232323; --color-bg-shade-4: #2D2D2D; - --color-bg-shade-5: #343434; + --color-bg-shade-5: #3A3A3A; // define all foreground color shades --color-fg-shade-1: #FFFFFF; @@ -18,14 +18,22 @@ --color-layout-icons-fg: var(--color-fg-shade-3); --color-layout-icons-hover-fg: var(--color-fg-shade-2); + // general text shades + --color-text: var(--color-fg-shade-1); + --color-text-dim: var(--color-fg-shade-2); + --color-text-dim-one: var(--color-fg-shade-3); + // menu shades --color-menu-bg: var(--color-bg-shade-3); --color-menu-item-hover-bg: var(--color-bg-shade-4); --color-menu-item-count-bg: var(--color-bg-shade-4); --color-menu-item-hover-count-bg: var(--color-bg-shade-5); - // general text shades - --color-text: var(--color-fg-shade-1); - --color-text-dim: var(--color-fg-shade-2); - --color-text-dim-one: var(--color-fg-shade-3); + // contextmenu shades + --color-contextmenu-bg: var(--color-bg-shade-4); + --color-contextmenu-border: var(--color-bg-shade-5); + --color-contextmenu-shadow: 3px 6px 12px rgba(0, 0, 0, 0.07); + --color-contextmenu-line: var(--color-bg-shade-5); + --color-contextmenu-item-bg: transparent; + --color-contextmenu-item-hover-bg: var(--color-bg-shade-5); } \ No newline at end of file diff --git a/Just/src/utils/cache.ts b/Just/src/utils/cache.ts new file mode 100644 index 0000000..bf16c07 --- /dev/null +++ b/Just/src/utils/cache.ts @@ -0,0 +1,3 @@ + +var cache: Map = new Map(); +export default cache; \ No newline at end of file diff --git a/Just/src/utils/composables/index.ts b/Just/src/utils/composables/index.ts new file mode 100644 index 0000000..7e714bb --- /dev/null +++ b/Just/src/utils/composables/index.ts @@ -0,0 +1,2 @@ + +export { useResize } from './useResize'; \ No newline at end of file diff --git a/Just/src/utils/composables/useResize.ts b/Just/src/utils/composables/useResize.ts new file mode 100644 index 0000000..a554f3f --- /dev/null +++ b/Just/src/utils/composables/useResize.ts @@ -0,0 +1,6 @@ +import { onMounted, onUnmounted } from 'vue' + +export function useResize(callback: () => void) { + onMounted(() => window.addEventListener('resize', callback)) + onUnmounted(() => window.removeEventListener('resize', callback)) +} \ No newline at end of file diff --git a/Just/src/utils/directives/index.ts b/Just/src/utils/directives/index.ts new file mode 100644 index 0000000..a47be48 --- /dev/null +++ b/Just/src/utils/directives/index.ts @@ -0,0 +1,7 @@ +import { type App } from 'vue'; +import clickOutside from './v-click-outside'; + +export function createDirectives (app: App) +{ + app.directive('click-outside', clickOutside); +} \ No newline at end of file diff --git a/Just/src/utils/directives/v-click-outside.ts b/Just/src/utils/directives/v-click-outside.ts new file mode 100644 index 0000000..4b20083 --- /dev/null +++ b/Just/src/utils/directives/v-click-outside.ts @@ -0,0 +1,130 @@ +const HANDLERS_PROPERTY = '__v-click-outside' +const HAS_WINDOWS = typeof window !== 'undefined' +const HAS_NAVIGATOR = typeof navigator !== 'undefined' +const IS_TOUCH = + HAS_WINDOWS && + ('ontouchstart' in window || + (HAS_NAVIGATOR && navigator.msMaxTouchPoints > 0)) +const EVENTS = IS_TOUCH ? ['touchstart'] : ['click'] + +function processDirectiveArguments(bindingValue) { + const isFunction = typeof bindingValue === 'function' + if (!isFunction && typeof bindingValue !== 'object') { + throw new Error( + 'v-click-outside: Binding value must be a function or an object', + ) + } + + return { + handler: isFunction ? bindingValue : bindingValue.handler, + middleware: bindingValue.middleware || ((item) => item), + events: bindingValue.events || EVENTS, + isActive: !(bindingValue.isActive === false), + detectIframe: !(bindingValue.detectIframe === false), + capture: !!bindingValue.capture, + } +} + +function execHandler({ event, handler, middleware }) { + if (middleware(event)) { + handler(event) + } +} + +function onFauxIframeClick({ el, event, handler, middleware }) { + // Note: on firefox clicking on iframe triggers blur, but only on + // next event loop it becomes document.activeElement + // https://stackoverflow.com/q/2381336#comment61192398_23231136 + setTimeout(() => { + const { activeElement } = document + if ( + activeElement && + activeElement.tagName === 'IFRAME' && + !el.contains(activeElement) + ) { + execHandler({ event, handler, middleware }) + } + }, 0) +} + +function onEvent({ el, event, handler, middleware }) { + // Note: composedPath is not supported on IE and Edge, more information here: + // https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath + // In the meanwhile, we are using el.contains for those browsers, not + // the ideal solution, but using IE or EDGE is not ideal either. + const path = (event.composedPath && event.composedPath()) || event.path + const isClickOutside = path + ? path.indexOf(el) < 0 + : !el.contains(event.target) + + if (!isClickOutside) { + return + } + + execHandler({ event, handler, middleware }) +} + +function bind(el, { value }) { + const { + events, + handler, + middleware, + isActive, + detectIframe, + capture, + } = processDirectiveArguments(value) + if (!isActive) { + return + } + + el[HANDLERS_PROPERTY] = events.map((eventName) => ({ + event: eventName, + srcTarget: document.documentElement, + handler: (event) => onEvent({ el, event, handler, middleware }), + capture, + })) + + if (detectIframe) { + const detectIframeEvent = { + event: 'blur', + srcTarget: window, + handler: (event) => onFauxIframeClick({ el, event, handler, middleware }), + capture, + } + + el[HANDLERS_PROPERTY] = [...el[HANDLERS_PROPERTY], detectIframeEvent] + } + + el[HANDLERS_PROPERTY].forEach(({ event, srcTarget, handler }) => + setTimeout(() => { + // Note: More info about this implementation can be found here: + // https://github.com/ndelvalle/v-click-outside/issues/137 + if (!el[HANDLERS_PROPERTY]) { + return + } + srcTarget.addEventListener(event, handler, capture) + }, 0), + ) +} + +function unbind(el) { + const handlers = el[HANDLERS_PROPERTY] || [] + handlers.forEach(({ event, srcTarget, handler, capture }) => + srcTarget.removeEventListener(event, handler, capture), + ) + delete el[HANDLERS_PROPERTY] +} + +function update(el, { value, oldValue }) { + if (JSON.stringify(value) === JSON.stringify(oldValue)) { + return + } + unbind(el) + bind(el, { value }) +} + +export default HAS_WINDOWS ? { + beforeMount: bind, + updated: update, + unmounted: unbind +} : {} \ No newline at end of file