Converts secrets list to something more generic
This commit is contained in:
@@ -5,13 +5,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta name="description" content="Plan and run your D&D sessions" />
|
||||||
name="description"
|
|
||||||
content="Web site created using create-tsrouter-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<title>Create TanStack App - .</title>
|
<title>Dungeon Master's Companion</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
219
package-lock.json
generated
219
package-lock.json
generated
@@ -6,6 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "lazy-dm",
|
"name": "lazy-dm",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^2.2.4",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@tanstack/react-query": "^5.77.2",
|
"@tanstack/react-query": "^5.77.2",
|
||||||
"@tanstack/react-router": "^1.114.3",
|
"@tanstack/react-router": "^1.114.3",
|
||||||
@@ -874,6 +875,79 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.0",
|
||||||
|
"@floating-ui/utils": "^0.2.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react": {
|
||||||
|
"version": "0.26.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
|
||||||
|
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.1.2",
|
||||||
|
"@floating-ui/utils": "^0.2.8",
|
||||||
|
"tabbable": "^6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@headlessui/react": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react": "^0.26.16",
|
||||||
|
"@react-aria/focus": "^3.20.2",
|
||||||
|
"@react-aria/interactions": "^3.25.0",
|
||||||
|
"@tanstack/react-virtual": "^3.13.9",
|
||||||
|
"use-sync-external-store": "^1.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/fs-minipass": {
|
"node_modules/@isaacs/fs-minipass": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||||
@@ -934,6 +1008,103 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-aria/focus": {
|
||||||
|
"version": "3.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.3.tgz",
|
||||||
|
"integrity": "sha512-rR5uZUMSY4xLHmpK/I8bP1V6vUNHFo33gTvrvNUsAKKqvMfa7R2nu5A6v97dr5g6tVH6xzpdkPsOJCWh90H2cw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-aria/interactions": "^3.25.1",
|
||||||
|
"@react-aria/utils": "^3.29.0",
|
||||||
|
"@react-types/shared": "^3.29.1",
|
||||||
|
"@swc/helpers": "^0.5.0",
|
||||||
|
"clsx": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-aria/interactions": {
|
||||||
|
"version": "3.25.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.1.tgz",
|
||||||
|
"integrity": "sha512-ntLrlgqkmZupbbjekz3fE/n3eQH2vhncx8gUp0+N+GttKWevx7jos11JUBjnJwb1RSOPgRUFcrluOqBp0VgcfQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-aria/ssr": "^3.9.8",
|
||||||
|
"@react-aria/utils": "^3.29.0",
|
||||||
|
"@react-stately/flags": "^3.1.1",
|
||||||
|
"@react-types/shared": "^3.29.1",
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-aria/ssr": {
|
||||||
|
"version": "3.9.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.8.tgz",
|
||||||
|
"integrity": "sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-aria/utils": {
|
||||||
|
"version": "3.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.0.tgz",
|
||||||
|
"integrity": "sha512-jSOrZimCuT1iKNVlhjIxDkAhgF7HSp3pqyT6qjg/ZoA0wfqCi/okmrMPiWSAKBnkgX93N8GYTLT3CIEO6WZe9Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-aria/ssr": "^3.9.8",
|
||||||
|
"@react-stately/flags": "^3.1.1",
|
||||||
|
"@react-stately/utils": "^3.10.6",
|
||||||
|
"@react-types/shared": "^3.29.1",
|
||||||
|
"@swc/helpers": "^0.5.0",
|
||||||
|
"clsx": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-stately/flags": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-stately/utils": {
|
||||||
|
"version": "3.10.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.6.tgz",
|
||||||
|
"integrity": "sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-types/shared": {
|
||||||
|
"version": "3.29.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.29.1.tgz",
|
||||||
|
"integrity": "sha512-KtM+cDf2CXoUX439rfEhbnEdAgFZX20UP2A35ypNIawR7/PFFPjQDWyA2EnClCcW/dLWJDEPX2U8+EJff8xqmQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.9",
|
"version": "1.0.0-beta.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz",
|
||||||
@@ -1201,6 +1372,15 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/helpers": {
|
||||||
|
"version": "0.5.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||||
|
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.7",
|
"version": "4.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz",
|
||||||
@@ -1567,6 +1747,23 @@
|
|||||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-virtual": {
|
||||||
|
"version": "3.13.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.9.tgz",
|
||||||
|
"integrity": "sha512-SPWC8kwG/dWBf7Py7cfheAPOxuvIv4fFQ54PdmYbg7CpXfsKxkucak43Q0qKsxVthhUJQ1A7CIMAIplq4BjVwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/virtual-core": "3.13.9"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/router-core": {
|
"node_modules/@tanstack/router-core": {
|
||||||
"version": "1.120.10",
|
"version": "1.120.10",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.120.10.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.120.10.tgz",
|
||||||
@@ -1725,6 +1922,16 @@
|
|||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/virtual-core": {
|
||||||
|
"version": "3.13.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.9.tgz",
|
||||||
|
"integrity": "sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/virtual-file-routes": {
|
"node_modules/@tanstack/virtual-file-routes": {
|
||||||
"version": "1.115.0",
|
"version": "1.115.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.115.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.115.0.tgz",
|
||||||
@@ -3482,6 +3689,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tabbable": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.7",
|
"version": "4.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz",
|
||||||
@@ -3679,6 +3892,12 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.19.4",
|
"version": "4.19.4",
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz",
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 3000",
|
"dev": "mprocs \"npm run start\" \"pocketbase serve\"",
|
||||||
"start": "vite --port 3000",
|
"start": "vite --port 3000",
|
||||||
"build": "vite build && tsc",
|
"build": "vite build && tsc",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"docker:build": "npm run docker:build:app && npm run docker:build:pocketbase"
|
"docker:build": "npm run docker:build:app && npm run docker:build:pocketbase"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^2.2.4",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@tanstack/react-query": "^5.77.2",
|
"@tanstack/react-query": "^5.77.2",
|
||||||
"@tanstack/react-router": "^1.114.3",
|
"@tanstack/react-router": "^1.114.3",
|
||||||
|
|||||||
115
src/components/DocumentList.tsx
Normal file
115
src/components/DocumentList.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import type { Document } from "@/lib/types";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
DialogTitle,
|
||||||
|
Transition,
|
||||||
|
TransitionChild,
|
||||||
|
} from "@headlessui/react";
|
||||||
|
import { Fragment, useState } from "react";
|
||||||
|
|
||||||
|
type Props<T extends Document> = {
|
||||||
|
title: React.ReactNode;
|
||||||
|
items: T[];
|
||||||
|
renderRow: (item: T) => React.ReactNode;
|
||||||
|
newItemForm: (onSubmit: () => void) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DocumentList is a generic list component for displaying document items with a dialog for adding new items.
|
||||||
|
*
|
||||||
|
* @param title - The title displayed above the list (left-aligned)
|
||||||
|
* @param items - The array of document items to display
|
||||||
|
* @param renderRow - Function to render each row's content
|
||||||
|
* @param newItemForm - Function that renders a form for creating a new item; receives an onSubmit callback
|
||||||
|
*/
|
||||||
|
export function DocumentList<T extends Document>({
|
||||||
|
title,
|
||||||
|
items,
|
||||||
|
renderRow,
|
||||||
|
newItemForm,
|
||||||
|
}: Props<T>) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
// Handles closing the dialog after form submission
|
||||||
|
const handleFormSubmit = (): void => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="w-full max-w-2xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-bold text-slate-100">{title}</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center rounded-full bg-violet-600 hover:bg-violet-700 text-white w-9 h-9 focus:outline-none focus:ring-2 focus:ring-violet-400"
|
||||||
|
aria-label="Add new item"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 4v16m8-8H4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.id} className="bg-slate-800 rounded p-4 text-slate-100">
|
||||||
|
{renderRow(item)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<Transition show={open} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-50" onClose={setOpen}>
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-200"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 transition-opacity" />
|
||||||
|
</TransitionChild>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<DialogPanel className="bg-slate-900 rounded-lg shadow-xl max-w-md w-full p-6 border border-slate-700 relative">
|
||||||
|
<DialogTitle className="text-lg font-semibold text-slate-100 mb-4">
|
||||||
|
Add New
|
||||||
|
</DialogTitle>
|
||||||
|
{newItemForm(handleFormSubmit)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute top-3 right-3 text-slate-400 hover:text-red-400 focus:outline-none"
|
||||||
|
aria-label="Close dialog"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -41,8 +41,13 @@ export type Secret = Document &
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export const RelationshipType = {
|
||||||
|
Secrets: "secrets",
|
||||||
|
DiscoveredIn: "discoveredIn",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export type Relationship = RecordModel & {
|
export type Relationship = RecordModel & {
|
||||||
primary: DocumentId;
|
primary: DocumentId;
|
||||||
secondary: DocumentId[];
|
secondary: DocumentId[];
|
||||||
type: "plannedSecrets" | "discoveredIn";
|
type: (typeof RelationshipType)[keyof typeof RelationshipType];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import { createFileRoute } from "@tanstack/react-router";
|
|||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
|
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Secret } from "@/lib/types";
|
import { RelationshipType, type Secret } from "@/lib/types";
|
||||||
|
import { DocumentList } from "@/components/DocumentList";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticated/document/$documentId")({
|
export const Route = createFileRoute("/_authenticated/document/$documentId")({
|
||||||
loader: async ({ params }) => {
|
loader: async ({ params }) => {
|
||||||
const doc = await pb.collection("documents").getOne(params.documentId);
|
const doc = await pb.collection("documents").getOne(params.documentId);
|
||||||
// Fetch the unique relationship where this document is the primary and type is "plannedSecrets"
|
// Fetch the unique relationship where this document is the primary and type is "plannedSecrets"
|
||||||
const relationships = await pb.collection("relationships").getList(1, 1, {
|
const relationships = await pb.collection("relationships").getList(1, 1, {
|
||||||
filter: `primary = "${params.documentId}" && type = "plannedSecrets"`,
|
filter: `primary = "${params.documentId}" && type = "${RelationshipType.Secrets}"`,
|
||||||
});
|
});
|
||||||
// Get all related secret document IDs from the secondary field
|
// Get all related secret document IDs from the secondary field
|
||||||
const secretIds =
|
const secretIds =
|
||||||
@@ -31,7 +32,7 @@ function RouteComponent() {
|
|||||||
const strongStart = session?.data?.session?.strongStart || "";
|
const strongStart = session?.data?.session?.strongStart || "";
|
||||||
const [newSecret, setNewSecret] = useState("");
|
const [newSecret, setNewSecret] = useState("");
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
const [secretList, setSecretList] = useState(secrets);
|
const [secretList, setSecretList] = useState<Secret[]>(secrets);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
async function handleSaveStrongStart(newValue: string) {
|
async function handleSaveStrongStart(newValue: string) {
|
||||||
@@ -63,7 +64,7 @@ function RouteComponent() {
|
|||||||
});
|
});
|
||||||
// 2. Check for existing relationship
|
// 2. Check for existing relationship
|
||||||
const existing = await pb.collection("relationships").getFullList({
|
const existing = await pb.collection("relationships").getFullList({
|
||||||
filter: `primary = "${session.id}" && type = "plannedSecrets"`,
|
filter: `primary = "${session.id}" && type = "${RelationshipType.Secrets}"`,
|
||||||
});
|
});
|
||||||
if (existing.length > 0) {
|
if (existing.length > 0) {
|
||||||
// Update existing relationship to add new secret to secondary array
|
// Update existing relationship to add new secret to secondary array
|
||||||
@@ -75,7 +76,7 @@ function RouteComponent() {
|
|||||||
await pb.collection("relationships").create({
|
await pb.collection("relationships").create({
|
||||||
primary: session.id,
|
primary: session.id,
|
||||||
secondary: [secretDoc.id],
|
secondary: [secretDoc.id],
|
||||||
type: "plannedSecrets",
|
type: RelationshipType.Secrets,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setSecretList([...secretList, secretDoc]);
|
setSecretList([...secretList, secretDoc]);
|
||||||
@@ -132,12 +133,40 @@ function RouteComponent() {
|
|||||||
placeholder="Enter a strong start for this session..."
|
placeholder="Enter a strong start for this session..."
|
||||||
aria-label="Strong Start"
|
aria-label="Strong Start"
|
||||||
/>
|
/>
|
||||||
<h3 className="text-lg font-semibold mt-8 mb-2 text-slate-200">
|
{secretList && (
|
||||||
Planned Secrets
|
<DocumentList
|
||||||
</h3>
|
title="Secrets and Clues"
|
||||||
{secretList && secretList.length > 0 ? (
|
items={secretList}
|
||||||
<ul className="space-y-2">
|
newItemForm={(onSubmit) => (
|
||||||
{secretList.map((secret: any) => (
|
<form
|
||||||
|
className="flex items-center gap-2 mt-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddSecret();
|
||||||
|
onSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 px-3 py-2 rounded bg-slate-800 text-slate-100 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500"
|
||||||
|
placeholder="Add a new secret..."
|
||||||
|
value={newSecret}
|
||||||
|
onChange={(e) => setNewSecret(e.target.value)}
|
||||||
|
disabled={adding}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-400 mt-2 text-sm">{error}</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 rounded bg-emerald-600 hover:bg-emerald-700 text-white font-semibold transition-colors disabled:opacity-60"
|
||||||
|
disabled={adding || !newSecret.trim()}
|
||||||
|
>
|
||||||
|
{adding ? "Adding..." : "Add Secret"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
renderRow={(secret) => (
|
||||||
<li
|
<li
|
||||||
key={secret.id}
|
key={secret.id}
|
||||||
className="bg-slate-800 rounded p-4 text-slate-100 flex items-center gap-3"
|
className="bg-slate-800 rounded p-4 text-slate-100 flex items-center gap-3"
|
||||||
@@ -159,37 +188,9 @@ function RouteComponent() {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
)}
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<div className="text-slate-400">
|
|
||||||
No planned secrets for this session.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<form
|
|
||||||
className="flex items-center gap-2 mt-4"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleAddSecret();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="flex-1 px-3 py-2 rounded bg-slate-800 text-slate-100 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500"
|
|
||||||
placeholder="Add a new secret..."
|
|
||||||
value={newSecret}
|
|
||||||
onChange={(e) => setNewSecret(e.target.value)}
|
|
||||||
disabled={adding}
|
|
||||||
/>
|
/>
|
||||||
<button
|
)}
|
||||||
type="submit"
|
|
||||||
className="px-4 py-2 rounded bg-emerald-600 hover:bg-emerald-700 text-white font-semibold transition-colors disabled:opacity-60"
|
|
||||||
disabled={adding || !newSecret.trim()}
|
|
||||||
>
|
|
||||||
{adding ? "Adding..." : "Add Secret"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{error && <div className="text-red-400 mt-2 text-sm">{error}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user