diff --git a/package-lock.json b/package-lock.json
index 54e488c..76ff3d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"clipboard-copy": "^4.0.1",
+ "cytoscape": "^3.23.0",
"qrcode-svg": "^1.1.0",
"uint8arrays": "^3.1.0",
"webnative": "^0.34.1"
@@ -18,6 +19,7 @@
"@sveltejs/kit": "1.0.0-next.489",
"@tailwindcss/typography": "^0.5.2",
"@terminusdb/terminusdb-client": "^10.0.25",
+ "@types/cytoscape": "^3.19.9",
"@types/qrcode-svg": "^1.1.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
@@ -1663,6 +1665,12 @@
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
"dev": true
},
+ "node_modules/@types/cytoscape": {
+ "version": "3.19.9",
+ "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.19.9.tgz",
+ "integrity": "sha512-oqCx0ZGiBO0UESbjgq052vjDAy2X53lZpMrWqiweMpvVwKw/2IiYDdzPFK6+f4tMfdv9YKEM9raO5bAZc3UYBg==",
+ "dev": true
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@@ -3183,6 +3191,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/cytoscape": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.23.0.tgz",
+ "integrity": "sha512-gRZqJj/1kiAVPkrVFvz/GccxsXhF3Qwpptl32gKKypO4IlqnKBjTOu+HbXtEggSGzC5KCaHp3/F7GgENrtsFkA==",
+ "dependencies": {
+ "heap": "^0.2.6",
+ "lodash": "^4.17.21"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/daisyui": {
"version": "2.19.0",
"integrity": "sha512-lLOz4cHm3xpm0AfdFojDFrhiDu4hZTdEbcVJri+KzQn1HvxmZS4STVujn8tV4RQXjchGdnIsXFqxd6F7rVZBiA==",
@@ -4572,6 +4592,11 @@
"node": ">=8"
}
},
+ "node_modules/heap": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
+ "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="
+ },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -10250,6 +10275,12 @@
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
"dev": true
},
+ "@types/cytoscape": {
+ "version": "3.19.9",
+ "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.19.9.tgz",
+ "integrity": "sha512-oqCx0ZGiBO0UESbjgq052vjDAy2X53lZpMrWqiweMpvVwKw/2IiYDdzPFK6+f4tMfdv9YKEM9raO5bAZc3UYBg==",
+ "dev": true
+ },
"@types/graceful-fs": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
@@ -11298,6 +11329,15 @@
"array-find-index": "^1.0.1"
}
},
+ "cytoscape": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.23.0.tgz",
+ "integrity": "sha512-gRZqJj/1kiAVPkrVFvz/GccxsXhF3Qwpptl32gKKypO4IlqnKBjTOu+HbXtEggSGzC5KCaHp3/F7GgENrtsFkA==",
+ "requires": {
+ "heap": "^0.2.6",
+ "lodash": "^4.17.21"
+ }
+ },
"daisyui": {
"version": "2.19.0",
"integrity": "sha512-lLOz4cHm3xpm0AfdFojDFrhiDu4hZTdEbcVJri+KzQn1HvxmZS4STVujn8tV4RQXjchGdnIsXFqxd6F7rVZBiA==",
@@ -12221,6 +12261,11 @@
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
+ "heap": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
+ "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="
+ },
"html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
diff --git a/package.json b/package.json
index b4537a1..a652036 100644
--- a/package.json
+++ b/package.json
@@ -12,10 +12,11 @@
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
- "@terminusdb/terminusdb-client": "^10.0.25",
"@sveltejs/adapter-static": "1.0.0-next.43",
"@sveltejs/kit": "1.0.0-next.489",
"@tailwindcss/typography": "^0.5.2",
+ "@terminusdb/terminusdb-client": "^10.0.25",
+ "@types/cytoscape": "^3.19.9",
"@types/qrcode-svg": "^1.1.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
@@ -55,6 +56,7 @@
},
"dependencies": {
"clipboard-copy": "^4.0.1",
+ "cytoscape": "^3.23.0",
"qrcode-svg": "^1.1.0",
"uint8arrays": "^3.1.0",
"webnative": "^0.34.1"
diff --git a/src/components/explore/Explore.svelte b/src/components/explore/Explore.svelte
new file mode 100644
index 0000000..5675b08
--- /dev/null
+++ b/src/components/explore/Explore.svelte
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/explore/data.json b/src/components/explore/data.json
new file mode 100644
index 0000000..85b7d15
--- /dev/null
+++ b/src/components/explore/data.json
@@ -0,0 +1,92 @@
+{
+ "entities": [{
+ "label": "Organization",
+ "title": "Neuralink"
+ }, {
+ "label": "Organization",
+ "title": "SpaceX"
+ }, {
+ "label": "Organization",
+ "title": "Pretoria"
+ }, {
+ "label": "Organization",
+ "title": "The Boring Company"
+ }, {
+ "label": "Organization",
+ "title": "University of Pretoria"
+ }, {
+ "label": "Organization",
+ "title": "Stanford University"
+ }, {
+ "label": "Person",
+ "title": "Jeff Bezos"
+ }, {
+ "label": "Organization",
+ "title": "University of Pennsylvania"
+ }, {
+ "label": "Person",
+ "title": "Kimbal Musk"
+ }, {
+ "label": "Organization",
+ "title": "Tesla, Inc."
+ }, {
+ "label": "Person",
+ "title": "Elon Musk"
+ }],
+ "relations": [{
+ "source": "Elon Musk",
+ "target": "Neuralink"
+ }, {
+ "source": "Tesla, Inc.",
+ "target": "Elon Musk",
+ "type": "owned by"
+ }, {
+ "source": "Elon Musk",
+ "target": "University of Pennsylvania",
+ "type": "residence"
+ }, {
+ "source": "Elon Musk",
+ "target": "Tesla, Inc.",
+ "type": "owned by"
+ }, {
+ "source": "The Boring Company",
+ "target": "Tesla, Inc.",
+ "type": "owned by"
+ }, {
+ "source": "Elon Musk",
+ "target": "Kimbal Musk",
+ "type": "sibling"
+ }, {
+ "source": "University of Pennsylvania",
+ "target": "Elon Musk",
+ "type": "residence"
+ }, {
+ "source": "The Boring Company",
+ "target": "Neuralink",
+ "type": "subsidiary"
+ }, {
+ "source": "Elon Musk",
+ "target": "University of Pretoria",
+ "type": "work location"
+ }, {
+ "source": "The Boring Company",
+ "target": "Elon Musk",
+ "type": "owned by"
+ }, {
+ "source": "Kimbal Musk",
+ "target": "Elon Musk",
+ "type": "sibling"
+ }, {
+ "source": "Neuralink",
+ "target": "Elon Musk",
+ "type": "owned by"
+ }, {
+ "source": "Elon Musk",
+ "target": "The Boring Company",
+ "type": "owned by"
+ }, {
+ "source": "Elon Musk",
+ "target": "University of Pennsylvania",
+ "type": "work location"
+ }]
+}
\ No newline at end of file
diff --git a/src/components/nav/SidebarNav.svelte b/src/components/nav/SidebarNav.svelte
index e74f45d..2782eac 100644
--- a/src/components/nav/SidebarNav.svelte
+++ b/src/components/nav/SidebarNav.svelte
@@ -20,6 +20,11 @@
href: '/gallery/',
icon: PhotoGallery
},
+ {
+ label: 'Explore',
+ href: '/explore/',
+ icon: PhotoGallery
+ },
{
label: 'About This Template',
href: '/about/',
diff --git a/src/routes/explore/+page.svelte b/src/routes/explore/+page.svelte
new file mode 100644
index 0000000..e0cf4bf
--- /dev/null
+++ b/src/routes/explore/+page.svelte
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/routes/explore/components/icons/FileUploadIcon.svelte b/src/routes/explore/components/icons/FileUploadIcon.svelte
new file mode 100644
index 0000000..afc4085
--- /dev/null
+++ b/src/routes/explore/components/icons/FileUploadIcon.svelte
@@ -0,0 +1,9 @@
+
diff --git a/src/routes/explore/components/imageGallery/ImageCard.svelte b/src/routes/explore/components/imageGallery/ImageCard.svelte
new file mode 100644
index 0000000..2fa81c5
--- /dev/null
+++ b/src/routes/explore/components/imageGallery/ImageCard.svelte
@@ -0,0 +1,24 @@
+
+
+
+
+
+

+
+
diff --git a/src/routes/explore/components/imageGallery/ImageGallery.svelte b/src/routes/explore/components/imageGallery/ImageGallery.svelte
new file mode 100644
index 0000000..f9a235d
--- /dev/null
+++ b/src/routes/explore/components/imageGallery/ImageGallery.svelte
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+ {#each $galleryStore.selectedArea === AREAS.PRIVATE ? $galleryStore.privateImages : $galleryStore.publicImages as image}{/each}
+
+
+
+
+ {#if selectedImage}
+
+ {/if}
+
diff --git a/src/routes/explore/components/imageGallery/ImageModal.svelte b/src/routes/explore/components/imageGallery/ImageModal.svelte
new file mode 100644
index 0000000..5c4f17a
--- /dev/null
+++ b/src/routes/explore/components/imageGallery/ImageModal.svelte
@@ -0,0 +1,162 @@
+
+
+
+
+{#if !!image}
+
+
+
+{/if}
diff --git a/src/routes/explore/components/imageGallery/MyseeliaPublic.svelte b/src/routes/explore/components/imageGallery/MyseeliaPublic.svelte
new file mode 100644
index 0000000..55a3366
--- /dev/null
+++ b/src/routes/explore/components/imageGallery/MyseeliaPublic.svelte
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+ {#each $galleryStore.selectedArea === AREAS.PRIVATE ? $galleryStore.privateImages : $galleryStore.publicImages as image}
+
+ {/each}
+
+
+
+ {#if selectedImage}
+
+ {/if}
+
\ No newline at end of file
diff --git a/src/routes/explore/components/upload/Dropzone.svelte b/src/routes/explore/components/upload/Dropzone.svelte
new file mode 100644
index 0000000..b4b4e3c
--- /dev/null
+++ b/src/routes/explore/components/upload/Dropzone.svelte
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/src/routes/explore/components/upload/FileUploadCard.svelte b/src/routes/explore/components/upload/FileUploadCard.svelte
new file mode 100644
index 0000000..b23ae44
--- /dev/null
+++ b/src/routes/explore/components/upload/FileUploadCard.svelte
@@ -0,0 +1,40 @@
+
+
+
+ {#if $galleryStore.loading}
+
+ {:else}
+
+
+
+ Upload a photo
+
+
SVG, PNG, JPG or GIF
+
+
+ {/if}
+
diff --git a/src/routes/explore/data.json b/src/routes/explore/data.json
new file mode 100644
index 0000000..85b7d15
--- /dev/null
+++ b/src/routes/explore/data.json
@@ -0,0 +1,92 @@
+{
+ "entities": [{
+ "label": "Organization",
+ "title": "Neuralink"
+ }, {
+ "label": "Organization",
+ "title": "SpaceX"
+ }, {
+ "label": "Organization",
+ "title": "Pretoria"
+ }, {
+ "label": "Organization",
+ "title": "The Boring Company"
+ }, {
+ "label": "Organization",
+ "title": "University of Pretoria"
+ }, {
+ "label": "Organization",
+ "title": "Stanford University"
+ }, {
+ "label": "Person",
+ "title": "Jeff Bezos"
+ }, {
+ "label": "Organization",
+ "title": "University of Pennsylvania"
+ }, {
+ "label": "Person",
+ "title": "Kimbal Musk"
+ }, {
+ "label": "Organization",
+ "title": "Tesla, Inc."
+ }, {
+ "label": "Person",
+ "title": "Elon Musk"
+ }],
+ "relations": [{
+ "source": "Elon Musk",
+ "target": "Neuralink"
+ }, {
+ "source": "Tesla, Inc.",
+ "target": "Elon Musk",
+ "type": "owned by"
+ }, {
+ "source": "Elon Musk",
+ "target": "University of Pennsylvania",
+ "type": "residence"
+ }, {
+ "source": "Elon Musk",
+ "target": "Tesla, Inc.",
+ "type": "owned by"
+ }, {
+ "source": "The Boring Company",
+ "target": "Tesla, Inc.",
+ "type": "owned by"
+ }, {
+ "source": "Elon Musk",
+ "target": "Kimbal Musk",
+ "type": "sibling"
+ }, {
+ "source": "University of Pennsylvania",
+ "target": "Elon Musk",
+ "type": "residence"
+ }, {
+ "source": "The Boring Company",
+ "target": "Neuralink",
+ "type": "subsidiary"
+ }, {
+ "source": "Elon Musk",
+ "target": "University of Pretoria",
+ "type": "work location"
+ }, {
+ "source": "The Boring Company",
+ "target": "Elon Musk",
+ "type": "owned by"
+ }, {
+ "source": "Kimbal Musk",
+ "target": "Elon Musk",
+ "type": "sibling"
+ }, {
+ "source": "Neuralink",
+ "target": "Elon Musk",
+ "type": "owned by"
+ }, {
+ "source": "Elon Musk",
+ "target": "The Boring Company",
+ "type": "owned by"
+ }, {
+ "source": "Elon Musk",
+ "target": "University of Pennsylvania",
+ "type": "work location"
+ }]
+}
\ No newline at end of file
diff --git a/src/routes/explore/lib/gallery.ts b/src/routes/explore/lib/gallery.ts
new file mode 100644
index 0000000..681ce04
--- /dev/null
+++ b/src/routes/explore/lib/gallery.ts
@@ -0,0 +1,209 @@
+import { get as getStore } from 'svelte/store'
+import * as wn from 'webnative'
+import * as uint8arrays from 'uint8arrays'
+import type { CID } from 'multiformats/cid'
+import type { PuttableUnixTree, File as WNFile } from 'webnative/fs/types'
+import type { Metadata } from 'webnative/fs/metadata'
+
+import { filesystemStore } from '$src/stores'
+import { AREAS, galleryStore } from '$routes/gallery/stores'
+import { addNotification } from '$lib/notifications'
+
+export type Image = {
+ cid: string
+ ctime: number
+ name: string
+ private: boolean
+ size: number
+ src: string
+}
+
+export type Gallery = {
+ publicImages: Image[] | null
+ privateImages: Image[] | null
+ selectedArea: AREAS
+ loading: boolean
+}
+
+interface GalleryFile extends PuttableUnixTree, WNFile {
+ cid: CID
+ content: Uint8Array
+ header: {
+ content: Uint8Array
+ metadata: Metadata
+ }
+}
+
+type Link = {
+ size: number
+}
+
+export const GALLERY_DIRS = {
+ [AREAS.PUBLIC]: ['public', 'gallery'],
+ [AREAS.PRIVATE]: ['private', 'gallery']
+}
+const FILE_SIZE_LIMIT = 5
+
+/**
+ * Get images from the user's WNFS and construct the `src` value for the images
+ */
+export const getImagesFromWNFS: () => Promise = async () => {
+ try {
+ // Set loading: true on the galleryStore
+ galleryStore.update(store => ({ ...store, loading: true }))
+
+ const { selectedArea } = getStore(galleryStore)
+ const isPrivate = selectedArea === AREAS.PRIVATE
+ const fs = getStore(filesystemStore)
+
+ // Set path to either private or public gallery dir
+ const path = wn.path.directory(...GALLERY_DIRS[selectedArea])
+
+ // Get list of links for files in the gallery dir
+ const links = await fs.ls(path)
+
+ const images = await Promise.all(
+ Object.entries(links).map(async ([name]) => {
+ const file = await fs.get(
+ wn.path.file(...GALLERY_DIRS[selectedArea], `${name}`)
+ )
+
+ // The CID for private files is currently located in `file.header.content`,
+ // whereas the CID for public files is located in `file.cid`
+ const cid = isPrivate
+ ? (file as GalleryFile).header.content.toString()
+ : (file as GalleryFile).cid.toString()
+
+ // Create a base64 string to use as the image `src`
+ const src = `data:image/jpeg;base64, ${uint8arrays.toString(
+ (file as GalleryFile).content,
+ 'base64'
+ )}`
+
+ return {
+ cid,
+ ctime: (file as GalleryFile).header.metadata.unixMeta.ctime,
+ name,
+ private: isPrivate,
+ size: (links[name] as Link).size,
+ src
+ }
+ })
+ )
+
+ // Sort images by ctime(created at date)
+ // NOTE: this will eventually be controlled via the UI
+ images.sort((a, b) => b.ctime - a.ctime)
+
+ // Push images to the galleryStore
+ galleryStore.update(store => ({
+ ...store,
+ ...(isPrivate
+ ? {
+ privateImages: images
+ }
+ : {
+ publicImages: images
+ }),
+ loading: false
+ }))
+ } catch (error) {
+ console.error(error)
+ galleryStore.update(store => ({
+ ...store,
+ loading: false
+ }))
+ }
+}
+
+/**
+ * Upload an image to the user's private or public WNFS
+ * @param image
+ */
+export const uploadImageToWNFS: (
+ image: File
+) => Promise = async image => {
+ try {
+ const { selectedArea } = getStore(galleryStore)
+ const fs = getStore(filesystemStore)
+
+ // Reject files over 5MB
+ const imageSizeInMB = image.size / (1024 * 1024)
+ if (imageSizeInMB > FILE_SIZE_LIMIT) {
+ throw new Error('Image can be no larger than 5MB')
+ }
+
+ // Reject the upload if the image already exists in the directory
+ const imageExists = await fs.exists(
+ wn.path.file(...GALLERY_DIRS[selectedArea], image.name)
+ )
+ if (imageExists) {
+ throw new Error(`${image.name} image already exists`)
+ }
+
+ // Create a sub directory and add some content
+ await fs.write(
+ wn.path.file(...GALLERY_DIRS[selectedArea], image.name),
+ image
+ )
+
+ // Announce the changes to the server
+ await fs.publish()
+
+ addNotification(`${image.name} image has been published`, 'success')
+ } catch (error) {
+ addNotification(error.message, 'error')
+ console.error(error)
+ }
+}
+
+/**
+ * Delete an image from the user's private or public WNFS
+ * @param name
+ */
+export const deleteImageFromWNFS: (
+ name: string
+) => Promise = async name => {
+ try {
+ const { selectedArea } = getStore(galleryStore)
+ const fs = getStore(filesystemStore)
+
+ const imageExists = await fs.exists(
+ wn.path.file(...GALLERY_DIRS[selectedArea], name)
+ )
+
+ if (imageExists) {
+ // Remove images from server
+ await fs.rm(wn.path.file(...GALLERY_DIRS[selectedArea], name))
+
+ // Announce the changes to the server
+ await fs.publish()
+
+ addNotification(`${name} image has been deleted`, 'success')
+
+ // Refetch images and update galleryStore
+ await getImagesFromWNFS()
+ } else {
+ throw new Error(`${name} image has already been deleted`)
+ }
+ } catch (error) {
+ addNotification(error.message, 'error')
+ console.error(error)
+ }
+}
+
+/**
+ * Handle uploads made by interacting with the file input directly
+ */
+export const handleFileInput: (
+ files: FileList
+) => Promise = async files => {
+ await Promise.all(
+ Array.from(files).map(async file => {
+ await uploadImageToWNFS(file)
+ })
+ )
+
+ // Refetch images and update galleryStore
+ await getImagesFromWNFS()
+}
diff --git a/tsconfig.json b/tsconfig.json
index 5fcef3d..67a2bc1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,13 @@
{
"compilerOptions": {
"moduleResolution": "node",
- "module": "es2020",
+ "jsx": "react",
+ "allowSyntheticDefaultImports": true, // that is probably the important one
+ "esModuleInterop": true, // this also
+ "typeRoots": [
+ "./node_modules/@types/"
+ ],
+ "module": "esnext",
"lib": ["es2020"],
"target": "es2019",
/**
@@ -16,7 +22,6 @@
enable source maps by default.
*/
"sourceMap": true,
- "esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",