commit a8e3eaea46ef3f3fe088dc7cd9bde3123b779865 Author: thepaperpilot Date: Sun Apr 7 17:02:19 2024 -0500 WIP diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..cf64a52 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +.eslintrc.cjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..b13d1ba --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,56 @@ +require("@rushstack/eslint-patch/modern-module-resolution"); + +module.exports = { + root: true, + env: { + node: true + }, + parser: '@typescript-eslint/parser', + plugins: ["@typescript-eslint"], + overrides: [ + { + files: ['*.ts', '*.tsx'], + extends: [ + "plugin:vue/vue3-essential", + "@vue/eslint-config-typescript/recommended", + "@vue/eslint-config-prettier" + ], + parserOptions: [ + { + ecmaVersion: 2020, + project: "./tsconfig.json" + }, + { + ecmaVersion: 2020, + project: "./tsconfig.node.json" + }, + ], + } + ], + ignorePatterns: ["src/lib"], + rules: { + "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", + "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", + "vue/script-setup-uses-vars": "warn", + "vue/no-mutating-props": "off", + "vue/multi-word-component-names": "off", + "@typescript-eslint/strict-boolean-expressions": [ + "error", + { + allowNullableObject: true, + allowNullableBoolean: true + } + ], + "eqeqeq": [ + "error", + "always", + { + "null": "never" + } + ] + }, + globals: { + defineProps: "readonly", + defineEmits: "readonly" + } +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..008a2f9 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "arrowParens": "avoid", + "endOfLine": "auto", + "printWidth": 100, + "tabWidth": 4, + "trailingComma": "none" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..0bfecb0 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..60bb06d --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "commune-mock", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/vue-fontawesome": "latest-3", + "autoprefixer": "^10.4.19", + "flowbite": "^2.3.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "vue": "^3.4.21", + "vue-router": "4" + }, + "devDependencies": { + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-vue": "^5.0.4", + "@vue/eslint-config-prettier": "^9.0.0", + "@vue/eslint-config-typescript": "^13.0.0", + "eslint": "^8.6.0", + "typescript": "^5.2.2", + "prettier": "^3.2.5", + "vite": "^5.2.0", + "vue-tsc": "^2.0.6" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..66cb2a0 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/src/assets/vue.svg b/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Avatar.vue b/src/components/Avatar.vue new file mode 100644 index 0000000..e394cae --- /dev/null +++ b/src/components/Avatar.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/components/CategoryHeader.vue b/src/components/CategoryHeader.vue new file mode 100644 index 0000000..e309b27 --- /dev/null +++ b/src/components/CategoryHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/Item.vue b/src/components/Item.vue new file mode 100644 index 0000000..29698a0 --- /dev/null +++ b/src/components/Item.vue @@ -0,0 +1,80 @@ + + + + + \ No newline at end of file diff --git a/src/components/ItemGroup.vue b/src/components/ItemGroup.vue new file mode 100644 index 0000000..c69257b --- /dev/null +++ b/src/components/ItemGroup.vue @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/src/components/Modal.vue b/src/components/Modal.vue new file mode 100644 index 0000000..143703a --- /dev/null +++ b/src/components/Modal.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/src/components/Nav.vue b/src/components/Nav.vue new file mode 100644 index 0000000..5ecd4af --- /dev/null +++ b/src/components/Nav.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/src/components/Thread.vue b/src/components/Thread.vue new file mode 100644 index 0000000..90a0b30 --- /dev/null +++ b/src/components/Thread.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/src/components/Todo.vue b/src/components/Todo.vue new file mode 100644 index 0000000..5d6c6b2 --- /dev/null +++ b/src/components/Todo.vue @@ -0,0 +1,67 @@ + + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..f68c8ac --- /dev/null +++ b/src/main.ts @@ -0,0 +1,18 @@ +import { createApp } from "vue"; +import { createMemoryHistory, createRouter } from "vue-router"; +import type { RouteRecordRaw } from "vue-router"; +import "./style.css"; +import App from "./App.vue"; +import Todo from "./components/Todo.vue"; + +const routes: RouteRecordRaw[] = [ + { path: '/', component: Todo }, + { path: '/todo/:source(\\d+)?/:sourceItem(\\d+)?/:thread(\\d+)?', component: Todo }, +]; + +const router = createRouter({ + history: createMemoryHistory(), + routes +}); + +createApp(App).use(router).mount("#app"); diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..57550fb --- /dev/null +++ b/src/state.ts @@ -0,0 +1,294 @@ +import { computed, ref, watch } from "vue"; +import { mergeSortedLists } from "./utils"; + +export type SourceType = "email" | "matrix" | "xmpp" | "rss" | "site"; + +export interface Item { + source: number; + sourceItem: number; + updatedAt: number; + threads: Record; +} + +export interface ItemThread { + category?: number; + count: number; + preview: string; + contact?: number; + snoozedUntil?: number; + updatedAt: number; +} + +export interface SourceItem { + id: number; + title: string; + image?: string; + contacts?: number[]; + threads: Record; +} + +export interface SourceThread { + id: number; + title: string; + timeline: ThreadEvent[]; +} + +export interface MessageEvent { + type: "message"; + contact: number; + time: number; + message: string; +} + +export interface CreateThreadEvent { + type: "create-thread"; + contact: number; + time: number; + thread: number; +} + +export type ThreadEvent = MessageEvent | CreateThreadEvent; + +export type ContactStatus = "online" | "away" | "offline" | "unknown"; + +export interface Contact { + id: number; + name: string; + image?: string; + status: ContactStatus; +} + +export interface Source { + id: number; + name: string; + type: SourceType; + items: Record; + contacts: Record; + selfContact: number; +} + +export interface LogicalRule { + type: "any" | "all" | "none"; + rules: Rule[]; +} + +export interface IsDmRule { + type: "isDM"; + inverted: boolean; +} + +export type Rule = LogicalRule | IsDmRule; + +export type Priority = "urgent" | "notify" | "todo" | "garden"; + +export interface Category { + id: number; + name: string; + priority: Priority; + activeItems: Item[]; +} + +export interface ThreadRef { + source: number; + sourceItem: number; + thread: number; +} + +export const sources = ref>({ + 0: { + id: 0, + items: { + 0: { + id: 0, + title: "Test Person", + threads: { + 0: { + id: 0, + title: "Thread 1", + timeline: [ + { type: "message", contact: 0, time: Date.now(), message: "Howdy!" } + ] + } + } + }, + 1: { + id: 1, + title: "Announcements", + threads: { + 0: { + id: 0, + title: "Thread 2", + timeline: [ + { type: "message", contact: 0, time: Date.now(), message: "Howdy!" } + ] + } + } + }, + 2: { + id: 2, + title: "C", + threads: { + 0: { + id: 0, + title: "Thread 3", + timeline: [ + { type: "message", contact: 0, time: Date.now(), message: "Howdy!" } + ] + } + } + }, + 3: { + id: 3, + title: "Another Person", + threads: { + 0: { + id: 0, + title: "Thread 4", + timeline: [ + { type: "message", contact: 0, time: Date.now(), message: "Howdy!" }, + { type: "message", contact: 1, time: Date.now(), message: "Howdy back!" }, + { type: "message", contact: 0, time: Date.now(), message: "Howdy 2!" }, + { type: "message", contact: 0, time: Date.now(), message: "Howdy 3!" }, + { type: "create-thread", contact: 0, time: Date.now(), thread: 1 } + ] + }, + 1: { + id: 1, + title: "Side Thread", + timeline: [ + { type: "message", contact: 0, time: Date.now(), message: "Howdy!" } + ] + } + } + } + }, + name: "Test Source", + type: "matrix", + contacts: { + 0: { + id: 0, + name: "Test Person", + status: "online", + image: "https://picsum.photos/seed/avatar0/64" + }, + 1: { + id: 1, + name: "Me", + status: "online", + image: "https://picsum.photos/seed/avatar1/64" + } + }, + selfContact: 1 + } +}); +export const items = ref([ + { + source: 0, + sourceItem: 0, + updatedAt: Date.now(), + threads: { + 0: { + category: 0, + count: 1, + preview: "Did you see the announcement?", + contact: 0, + updatedAt: Date.now() + } + } + }, + { + source: 0, + sourceItem: 1, + updatedAt: Date.now() - 60 * 1000, + threads: { + 0: { + category: 0, + count: 2, + preview: "Fire!", + updatedAt: Date.now() - 60 * 1000, + } + } + }, + { + source: 0, + sourceItem: 3, + updatedAt: Date.now(), + threads: { + 0: { + category: 1, + count: 6, + preview: "Did you get that?", + contact: 0, + updatedAt: Date.now() + }, + 1: { + category: 1, + count: 2, + preview: "Let's circle back", + updatedAt: Date.now() + } + } + } +]); +export const unsortedItems = ref([]); +export const rules = ref<{ category?: string; rules: Rule[] }[]>([]); +export const categories = ref([ + { + id: 0, + name: "Urgent", + priority: "urgent", + activeItems: [] + }, + { + id: 1, + name: "DMs", + priority: "notify", + activeItems: [] + } +]); +export const favorites = ref([ + { source: 0, sourceItem: 2, thread: 0 }, + { source: 0, sourceItem: 1, thread: 0 }, + { source: 0, sourceItem: 3, thread: 0 } +]); +export const urgentItems = computed(() => + categories.value + .filter(cat => cat.priority === "urgent") + .reduce((acc, curr) => mergeSortedLists(acc, curr.activeItems), [] as Item[]) +); +export const todoItems = computed(() => + todoCategories.value + .reduce((acc, curr) => mergeSortedLists(acc, curr.activeItems), [] as Item[]) +); +export const todoCategories = computed(() => categories.value.filter(c => ["urgent", "notify", "todo"].includes(c.priority))); + +watch( + items, + items => { + const mappedItems: Record = {}; + const unsorted: Item[] = []; + + items.forEach(item => { + let includeInUnsorted = false; + const includeInCategories = new Set(); + Object.values(item.threads).forEach(t => { + if (t.category == null) { + includeInUnsorted = true; + } else { + includeInCategories.add(t.category); + } + }); + if (includeInUnsorted) { + unsorted.push(item); + } + includeInCategories.forEach(cat => mappedItems[cat] = [...(mappedItems[cat] ?? []), item]); + }); + + categories.value.forEach(cat => { + // Noting here that activeItems must be sorted by most recently updated + cat.activeItems = mappedItems[cat.id]?.sort((a, b) => a.updatedAt - b.updatedAt) ?? []; + }); + unsortedItems.value = unsorted; + }, + { immediate: true } +); diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..9dc7a22 --- /dev/null +++ b/src/style.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.center { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +* { + transition: all .2s ease; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..16a00f3 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,17 @@ +// Modified from https://www.basedash.com/blog/how-to-merge-two-sorted-lists-in-javascript +export function mergeSortedLists(arr1: Array, arr2: Array): Array { + const merged: Array = []; + let i = 0, + j = 0; + + while (i < arr1.length && j < arr2.length) { + if (arr1[i] < arr2[j]) { + merged.push(arr1[i++]); + } else { + merged.push(arr2[j++]); + } + } + + // Append any remaining elements from arr1 or arr2 + return [...merged, ...arr1.slice(i), ...arr2.slice(j)]; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..4d0911a --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,15 @@ + /** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + "./node_modules/flowbite/**/*.js" + ], + theme: { + extend: {} + }, + plugins: [ + require('flowbite/plugin') + ], + darkMode: 'selector', +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..89e3ce8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "esnext", + "useDefineForClassFields": true, + "module": "esnext", + "lib": ["esnext", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "importHelpers": true, + "moduleResolution": "node", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "vue", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..97ede7e --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..35966e0 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + base: "/" +})