From 66334751f71507a29b9a605244dc6f8246bd1ca6 Mon Sep 17 00:00:00 2001 From: lukeg90 Date: Fri, 14 Jul 2023 14:54:11 +0200 Subject: [PATCH 1/8] Add and display tags --- .../CTypeDetails/CTypeDetails.astro | 34 ++ .../CreateForm/CreateForm.module.css | 55 +++ src/components/CreateForm/CreateForm.test.tsx | 10 +- src/components/CreateForm/CreateForm.tsx | 108 +++++- .../__snapshots__/CreateForm.test.tsx.snap | 317 ++++++++++++++++++ src/components/CreateForm/xmark-solid.svg | 1 + src/components/Input/Input.module.css | 2 + src/layouts/layout.css | 1 + src/models/tag.ts | 2 +- src/pages/ctype.ts | 11 +- src/pages/ctype/[id].astro | 12 +- src/utilities/mockCTypes.ts | 1 + 12 files changed, 547 insertions(+), 7 deletions(-) create mode 100644 src/components/CreateForm/xmark-solid.svg diff --git a/src/components/CTypeDetails/CTypeDetails.astro b/src/components/CTypeDetails/CTypeDetails.astro index 3d2c7507..78f1eb53 100644 --- a/src/components/CTypeDetails/CTypeDetails.astro +++ b/src/components/CTypeDetails/CTypeDetails.astro @@ -25,6 +25,7 @@ interface Props { } const { + id, title, creator, properties, @@ -33,8 +34,11 @@ const { extrinsicHash, schema, attestationsCount, + tags, } = Astro.props.cTypeData; +const tagNames = tags?.map((tag) => tag.dataValues.tagName); + const web3Name = await getWeb3NameForDid(creator); const schemaV1 = CType.fromProperties('', {}).$schema; @@ -95,6 +99,25 @@ const { subscan } = configuration; {createdAt.toLocaleString()} + { + tagNames && tagNames.length > 0 && ( + +
Tags
+
+ +
+
+ ) + } +
Extrinsic:
diff --git a/src/components/CreateForm/CreateForm.module.css b/src/components/CreateForm/CreateForm.module.css index b9c36572..5ea1b4a4 100644 --- a/src/components/CreateForm/CreateForm.module.css +++ b/src/components/CreateForm/CreateForm.module.css @@ -29,6 +29,7 @@ inline-size: 100%; block-size: 6rem; font-family: inherit; + padding: 0.5rem 0.75rem; } .fieldset { @@ -42,6 +43,60 @@ cursor: pointer; } +.tags { + box-sizing: border-box; + border-radius: var(--border-radius); + font-family: inherit; + padding: 0.5rem 0.75rem; + background: white; + margin: 0; + list-style: none; + min-block-size: 2.5rem; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; +} + +.tag { + box-sizing: border-box; + display: inline-flex; + height: 1.5rem; + align-items: center; + background: var(--color-tag); + color: white; + border: 1px solid white; + border-radius: var(--border-radius); + padding-inline-start: 0.5rem; + gap: 0.25rem; +} + +.removeTag { + border: 0; + padding: 0; + width: 1.5rem; + height: 100%; + background: url('./xmark-solid.svg') no-repeat center/50%; + display: inline-flex; + cursor: pointer; +} + +.tagInputContainer { + flex-grow: 1; + flex-shrink: 0; +} + +.tagInput { + border: none; + outline: none; + width: 100%; +} + +.tagInputDescription { + margin: 0; + font-size: 0.75rem; +} + .submit { composes: primary from '../Button.module.css'; } diff --git a/src/components/CreateForm/CreateForm.test.tsx b/src/components/CreateForm/CreateForm.test.tsx index dc49146f..d725bb7d 100644 --- a/src/components/CreateForm/CreateForm.test.tsx +++ b/src/components/CreateForm/CreateForm.test.tsx @@ -14,7 +14,7 @@ vi.mocked(useSupportedExtensions).mockReturnValue([ describe('CreateForm', () => { it('should render', async () => { - const { container, queryAllByLabelText, getByText } = render( + const { container, queryAllByLabelText, getByText, getByRole } = render( , ); expect(container).toMatchSnapshot(); @@ -22,5 +22,13 @@ describe('CreateForm', () => { await userEvent.click(getByText(/Add Property/)); await waitFor(() => queryAllByLabelText('Type:').length === 1); expect(container).toMatchSnapshot(); + + await userEvent.type( + getByRole('textbox', { + name: 'Tags (Optional) Enter a comma after each tag', + }), + 'tag1 ,tag2, x, tag3,', + ); + expect(container).toMatchSnapshot(); }); }); diff --git a/src/components/CreateForm/CreateForm.tsx b/src/components/CreateForm/CreateForm.tsx index df0ae32b..eab3326d 100644 --- a/src/components/CreateForm/CreateForm.tsx +++ b/src/components/CreateForm/CreateForm.tsx @@ -1,4 +1,11 @@ -import { FormEvent, useCallback, useState } from 'react'; +import { + FocusEvent, + FormEvent, + KeyboardEvent, + MouseEvent, + useCallback, + useState, +} from 'react'; import { web3FromSource } from '@polkadot/extension-dapp'; import { Blockchain, @@ -28,6 +35,63 @@ export function CreateForm() { [propertiesCount], ); + const [tags, setTags] = useState([]); + + const addTag = useCallback( + (tag: string) => { + const trimmed = tag.trim().replace(/,/g, '').toLowerCase(); + if (!trimmed || trimmed.length < 2 || tags.includes(trimmed)) { + return; + } + setTags([...tags, trimmed]); + }, + [tags], + ); + + const handleTagInputKeyUp = useCallback( + (event: KeyboardEvent) => { + if (event.key !== ',') { + return; + } + addTag(event.currentTarget.value); + event.currentTarget.value = ''; + }, + [addTag], + ); + const handleTagInputBlur = useCallback( + (event: FocusEvent) => { + addTag(event.currentTarget.value); + event.currentTarget.value = ''; + }, + [addTag], + ); + const handleTagInputKeyDown = useCallback( + (event: KeyboardEvent) => { + if ( + event.key !== 'Backspace' || + event.currentTarget.value || + tags.length === 0 + ) { + return; + } + + const lastTag = tags.slice(-1)[0]; + setTags(tags.slice(0, -1)); + event.currentTarget.value = lastTag; + }, + [tags], + ); + + const handleRemoveTag = useCallback( + (tagToRemove: string) => (event: MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + console.log('removing'); + setTags(tags.filter((tag) => tag !== tagToRemove)); + }, + [tags], + ); + const [account, setAccount] = useState(); const [progress, setProgress] = useState(false); const [error, setError] = useState(false); @@ -85,7 +149,13 @@ export function CreateForm() { const extrinsicHash = signed.hash.toHex(); const response = await fetch(paths.ctypes, { method: 'POST', - body: JSON.stringify({ cType, extrinsicHash, creator, description }), + body: JSON.stringify({ + cType, + extrinsicHash, + creator, + description, + tags, + }), headers: { 'Content-Type': 'application/json' }, }); if (response.ok) { @@ -100,7 +170,7 @@ export function CreateForm() { await disconnect(); } }, - [account, propertiesCount], + [account, propertiesCount, tags], ); const extensions = useSupportedExtensions(); @@ -165,6 +235,38 @@ export function CreateForm() {

+ + {extensions.length === 0 &&

No wallets detected

} diff --git a/src/components/CreateForm/__snapshots__/CreateForm.test.tsx.snap b/src/components/CreateForm/__snapshots__/CreateForm.test.tsx.snap index 58e058a7..a8ada84e 100644 --- a/src/components/CreateForm/__snapshots__/CreateForm.test.tsx.snap +++ b/src/components/CreateForm/__snapshots__/CreateForm.test.tsx.snap @@ -78,6 +78,32 @@ exports[`CreateForm > should render 1`] = `

+

@@ -287,6 +313,297 @@ exports[`CreateForm > should render 2`] = `

+ +

+ + and + + +

+

+