diff --git a/package-lock.json b/package-lock.json index 64c477b..221a215 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@tiptap/react": "^2.6.6", "@tiptap/starter-kit": "^2.6.6", "astro": "^4.14.3", + "date-fns": "^4.1.0", "dexie": "^4.0.8", "dexie-react-hooks": "^1.1.7", "preline": "^2.4.1", @@ -7648,6 +7649,16 @@ "node": ">=18" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", diff --git a/package.json b/package.json index a9c71db..82769c9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@tiptap/react": "^2.6.6", "@tiptap/starter-kit": "^2.6.6", "astro": "^4.14.3", + "date-fns": "^4.1.0", "dexie": "^4.0.8", "dexie-react-hooks": "^1.1.7", "preline": "^2.4.1", diff --git a/src/application/SparkList/SparkList.tsx b/src/application/SparkList/SparkList.tsx index 49e45ff..8ac4979 100644 --- a/src/application/SparkList/SparkList.tsx +++ b/src/application/SparkList/SparkList.tsx @@ -1,14 +1,78 @@ import { useLiveQuery } from "dexie-react-hooks"; import { sparkService } from "../../scripts/db/SparkService"; +import { format } from "date-fns"; +import type Spark from "../../interfaces/Spark"; export const SparkList = () => { const sparks = useLiveQuery(() => sparkService.listSparks()); + const grouped = sparks?.reduce< + { + key: string; + prefixTags: string[]; + sparks: Spark[]; + }[] + >((tmpGrouped, spark) => { + const prevKey = + tmpGrouped.length > 0 + ? tmpGrouped[tmpGrouped.length - 1].key + : `${Math.random()}`; + const nextKey = + spark.contextTags.length > 0 + ? spark.contextTags.join("/") + : `${Math.random()}`; + if (prevKey === "" || prevKey !== nextKey) { + tmpGrouped.push({ + key: nextKey, + prefixTags: spark.contextTags, + sparks: [spark], + }); + return tmpGrouped; + } + + tmpGrouped[tmpGrouped.length - 1].sparks.push(spark); + return tmpGrouped; + }, []); + return ( - + ); }; diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 360cd07..88a5d11 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -8,7 +8,7 @@ const { title } = Astro.props; --- - + @@ -18,7 +18,7 @@ const { title } = Astro.props; {title} - + diff --git a/src/pages/index.astro b/src/pages/index.astro index 8dc4a45..466c59a 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -7,20 +7,17 @@ import Layout from "../layouts/BaseLayout.astro"; -
-

Sparktag - Under Construction

- -
- -
- Sparks - -
-
- -

sparktag@derwebcoder.de

+
+
+ +
+ +
+
diff --git a/src/scripts/db/SparkService.ts b/src/scripts/db/SparkService.ts index 87cc6a7..eb3a449 100644 --- a/src/scripts/db/SparkService.ts +++ b/src/scripts/db/SparkService.ts @@ -6,9 +6,10 @@ export class SparkService { constructor(private db: AppDB) {} public async addSpark(plainText: string, html: string) { - const [prefixTags, remainingTags] = extractTags(plainText); + const [prefixTags, remainingTags, strippedContent] = + extractTags(plainText); await this.db.sparks.add({ - plainText, + plainText: strippedContent, html, creationDate: Date.now(), tags: remainingTags, @@ -25,7 +26,10 @@ export class SparkService { } public async listSparks() { - return await this.db.sparks.toCollection().sortBy("creationDate"); + return await this.db.sparks + .toCollection() + .reverse() + .sortBy("creationDate"); } } diff --git a/src/scripts/utils/stringUtils.test.ts b/src/scripts/utils/stringUtils.test.ts index 00e5afc..0fa7880 100644 --- a/src/scripts/utils/stringUtils.test.ts +++ b/src/scripts/utils/stringUtils.test.ts @@ -5,43 +5,55 @@ describe("extractTags", () => { const content = "#tag1 #tag2 I am great #tag3"; const expectedPrefixTags = ["#tag1", "#tag2"]; const expectedRemainingTags = ["#tag3"]; + const expectStrippedContent = "I am great #tag3"; - const [prefixTags, remainingTags] = extractTags(content); + const [prefixTags, remainingTags, strippedContent] = + extractTags(content); expect(prefixTags).toEqual(expectedPrefixTags); expect(remainingTags).toEqual(expectedRemainingTags); + expect(strippedContent).toEqual(expectStrippedContent); }); test("returns empty arrays when there are no tags", () => { const content = "This is a sample text without any tags"; const expectedPrefixTags: string[] = []; const expectedRemainingTags: string[] = []; + const expectStrippedContent = "This is a sample text without any tags"; - const [prefixTags, remainingTags] = extractTags(content); + const [prefixTags, remainingTags, strippedContent] = + extractTags(content); expect(prefixTags).toEqual(expectedPrefixTags); expect(remainingTags).toEqual(expectedRemainingTags); + expect(strippedContent).toEqual(expectStrippedContent); }); test("returns empty arrays when the content is empty", () => { const content = ""; const expectedPrefixTags: string[] = []; const expectedRemainingTags: string[] = []; + const expectStrippedContent = ""; - const [prefixTags, remainingTags] = extractTags(content); + const [prefixTags, remainingTags, strippedContent] = + extractTags(content); expect(prefixTags).toEqual(expectedPrefixTags); expect(remainingTags).toEqual(expectedRemainingTags); + expect(strippedContent).toEqual(expectStrippedContent); }); test("returns empty prefixTags and correct remainingTags when there are no prefix tags and one or more remaining tags", () => { const content = "Hello world #tag1 #tag2 #tag3"; const expectedPrefixTags: string[] = []; const expectedRemainingTags = ["#tag1", "#tag2", "#tag3"]; + const expectStrippedContent = "Hello world #tag1 #tag2 #tag3"; - const [prefixTags, remainingTags] = extractTags(content); + const [prefixTags, remainingTags, strippedContent] = + extractTags(content); expect(prefixTags).toEqual(expectedPrefixTags); expect(remainingTags).toEqual(expectedRemainingTags); + expect(strippedContent).toEqual(expectStrippedContent); }); }); diff --git a/src/scripts/utils/stringUtils.ts b/src/scripts/utils/stringUtils.ts index 9acbb84..c90843e 100644 --- a/src/scripts/utils/stringUtils.ts +++ b/src/scripts/utils/stringUtils.ts @@ -5,18 +5,22 @@ * @returns An array containing two arrays: prefixTags and remainingTags. * - prefixTags: An array of tags that have a prefix. * - remainingTags: An array of tags without a prefix. + * - strippedContent: The content without the prefixTags */ -export const extractTags = (content: string): [string[], string[]] => { +export const extractTags = (content: string): [string[], string[], string] => { const token = content.split(" "); const prefixTags: string[] = []; + let prefixTagsLength = 0; while (token.length > 0 && token[0].startsWith("#")) { - console.log("look", token[0]); prefixTags.push(token[0]); + prefixTagsLength += token[0].length; token.shift(); } const remainingTags = token.filter((t) => t.startsWith("#")); - return [prefixTags, remainingTags]; + const strippedContent = content.slice(prefixTagsLength + prefixTags.length); + + return [prefixTags, remainingTags, strippedContent]; }; diff --git a/src/ui/components/TextInput/TextInput.config.tsx b/src/ui/components/TextInput/TextInput.config.tsx index 9e423ed..8afed80 100644 --- a/src/ui/components/TextInput/TextInput.config.tsx +++ b/src/ui/components/TextInput/TextInput.config.tsx @@ -4,11 +4,6 @@ import StarterKit from "@tiptap/starter-kit"; export const extensions = [ StarterKit.configure({ - paragraph: { - HTMLAttributes: { - class: "text-gray-800 dark:text-neutral-200", - }, - }, bold: { HTMLAttributes: { class: "font-bold", @@ -31,7 +26,7 @@ export const extensions = [ }, }), Placeholder.configure({ - placeholder: "Add a message, if you'd like.", + placeholder: "Your next spark", emptyNodeClass: "text-gray-400 dark:text-neutral-200", }), ListKeymap, diff --git a/src/ui/components/TextInput/TextInput.tsx b/src/ui/components/TextInput/TextInput.tsx index c29b314..edea428 100644 --- a/src/ui/components/TextInput/TextInput.tsx +++ b/src/ui/components/TextInput/TextInput.tsx @@ -1,6 +1,7 @@ import { EditorContent, useEditor, type Editor } from "@tiptap/react"; import { extensions } from "./TextInput.config"; import "./TextInput.css"; +import { extractTags } from "../../../scripts/utils/stringUtils"; export type TextInputProps = { onSubmit?: (plainText: string, html: string) => void; @@ -15,7 +16,7 @@ export const TextInput = (props: TextInputProps) => { editorProps: { attributes: { "aria-label": "Add a spark", - class: "p-4 block w-full border border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600", + class: "p-4 min-h-32 block w-full bg-white border border-blue-300 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600", }, handleKeyDown: (view, event) => { if (!onSubmit) { @@ -30,6 +31,10 @@ export const TextInput = (props: TextInputProps) => { return false; } onSubmit(plainText, html); + const [prefixTags] = extractTags(plainText); + editor?.commands.setContent(`${prefixTags.join(" ")} `, false, { + preserveWhitespace: true, + }); return true; }, }, diff --git a/tailwind.config.mjs b/tailwind.config.mjs index b35dbb1..212ff82 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -12,4 +12,5 @@ export default { require("@tailwindcss/forms"), require("preline/plugin"), ], + darkMode: "selector", };