start visualization with cytoscape

This commit is contained in:
Darren Zal 2023-02-01 14:58:36 -08:00
parent 77ea389b5c
commit c15f5a0da7
16 changed files with 1243 additions and 3 deletions

45
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -0,0 +1,223 @@
<script lang="ts">
import cytoscape from 'cytoscape';
import { onMount } from 'svelte'
let cy;
let cyDiv;
const createGraph = () => {
cy = cytoscape({
container: cyDiv,
elements: [],
style: [],
});
};
interface INodeData {
id: string;
}
interface INode {
data: INodeData;
}
interface IEdgeData {
id: string;
source: string;
target: string;
label: string;
}
interface IEdge {
data: IEdgeData;
}
let knowledgeGraphJson: any = {
"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"
}]
};
async function fetchData() {
const response = await fetch('./data.json');
if (response.ok) {
knowledgeGraphJson = await response.json();
} else {
alert(`HTTP-Error: ${response.status}`);
}
}
let nodes: INode[] = [];
let edges: IEdge[] = [];
onMount(async () => {
console.log("1");
cyDiv = document.getElementById('cy');
console.log("2");
//await fetchData();
console.log(knowledgeGraphJson);
nodes = knowledgeGraphJson.entities.map((entity: any) => ({
data: { id: entity.title },
}));
edges = knowledgeGraphJson.relations.map((relation: any, index: string) => ({
data: {
id: index,
source: relation.source,
target: relation.target,
label: relation.type,
},
}));
console.log(edges);
const cy = cytoscape({
container: document.getElementById('cy'),
elements: {
nodes,
edges,
},
style: [
{
selector: 'node',
style: {
'text-valign': 'center',
'text-halign': 'center',
label: 'data(id)',
width: 100,
height: 100,
},
},
{
selector: 'edge',
style: {
width: 5,
'line-color': 'light grey',
'target-arrow-color': 'grey',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
label: 'data(label)',
},
},
],
});
cy.layout({
name: 'cose',
}).run();
});
</script>
<style>
#cy {
width: 100%;
height: 100%;
position: absolute;
top: 55px;
left: 0px;
}
</style>
<section class="overflow-hidden text-gray-700">
<div class="pt-8 p-6 md:p-8 mx-auto">
</div>
<div id="cy"></div>
</section>

View File

@ -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"
}]
}

View File

@ -20,6 +20,11 @@
href: '/gallery/',
icon: PhotoGallery
},
{
label: 'Explore',
href: '/explore/',
icon: PhotoGallery
},
{
label: 'About This Template',
href: '/about/',

View File

@ -0,0 +1,7 @@
<script lang="ts">
import Explore from '$components/explore/Explore.svelte'
</script>
<Explore />

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="66" fill="none">
<path
stroke="#171717"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14.167 45.667C7.17 45.667 1.5 39.996 1.5 33c0-6.039 4.226-11.09 9.882-12.36A15.89 15.89 0 0 1 11 17.167c0-8.745 7.089-15.834 15.833-15.834 7.662 0 14.052 5.441 15.518 12.67.105-.002.21-.003.316-.003C51.41 14 58.5 21.089 58.5 29.833c0 7.66-5.44 14.05-12.667 15.517M39.5 36.167l-9.5-9.5m0 0-9.5 9.5m9.5-9.5v38"
/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -0,0 +1,24 @@
<script lang="ts">
import type { Image } from '$routes/gallery/lib/gallery'
export let image: Image
export let openModal: (image: Image) => void
const handleOpenModal = () => openModal(image)
</script>
<div
class="relative group w-full aspect-[22/23] rounded-lg border-2 border-transparent hover:border-base-content box-border overflow-hidden transition-colors ease-in"
on:click={handleOpenModal}
>
<div
class="flex items-center justify-center absolute z-10 top-0 right-0 bottom-0 left-0 bg-[#00000035] opacity-0 group-hover:opacity-100 transition-opacity ease-in"
/>
<div class="relative pb-[105%]">
<img
class="absolute block object-cover object-center w-full h-full"
alt={`Gallery Image: ${image.name}`}
src={image.src}
/>
</div>
</div>

View File

@ -0,0 +1,184 @@
<script lang="ts">
import { onDestroy } from 'svelte'
import { onMount } from 'svelte'
import TerminusClient from "@terminusdb/terminusdb-client"
import { filesystemStore, sessionStore } from '$src/stores'
import { AREAS, galleryStore } from '$routes/gallery/stores'
import { getImagesFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import FileUploadCard from '$routes/gallery/components/upload/FileUploadCard.svelte'
import ImageCard from '$routes/gallery/components/imageGallery/ImageCard.svelte'
import ImageModal from '$routes/gallery/components/imageGallery/ImageModal.svelte'
const client = new TerminusClient.WOQLClient(
"https://cloud.terminusdb.com/Myseelia",{
user:"zaldarren@gmail.com",
organization:"Myseelia",
db: "Myseelia",
token: "dGVybWludXNkYjovLy9kYXRhL2tleXNfYXBpLzg5OTY0ZGI5OWFlYjQ1Zjc5OGM5ZTRiZWI2MzExOGJhZjhiOWRiOWNlOTJiNmU2NGI0NDEzZjIzNDFmOGVkMjc=_869e9bd2465ad84126151962994fcfa22d4b7ec9375edf16b4182e7f36e4b2b820075ba22e78f629e0691eddbeae6998a6504d5ce287aa1df2602cb556b58e1730b0b93feb0e9304"
}
);
let username = $sessionStore.username;
let bioregion = '';
let ecozone = '';
let affiliatedOrganizations = "Organization/8c8368b55dc80f18ba254771701f6d1bc79a3a90f127c28b3145a2c2204e97ce";
let givenName = '';
let hasCredential = {};
onMount(async () => {
try{
await client.connect()
const schema = await client.getSchema("myseelia", "main")
// console.log("Schema");
// console.log(schema);
// console.log("result");
// const result = await client.getDocument({as_list:true,type:"Person",query: { userName: "tester" }})
// console.log(result);
}catch(err){
console.error("this is it" + err.message)
}
});
/**
* Open the ImageModal and pass it the selected `image` from the gallery
* @param image
*/
let selectedImage: Image
const setSelectedImage: (image: Image) => void = image =>
(selectedImage = image)
const clearSelectedImage = () => (selectedImage = null)
// If galleryStore.selectedArea changes from private to public, re-run getImagesFromWNFS
let selectedArea = null
const unsubscribeGalleryStore = galleryStore.subscribe(async updatedStore => {
// Get initial selectedArea
if (!selectedArea) {
selectedArea = updatedStore.selectedArea
}
if (selectedArea !== updatedStore.selectedArea) {
selectedArea = updatedStore.selectedArea
await getImagesFromWNFS()
}
})
// Once the user has been authed, fetch the images from their file system
let imagesFetched = false
const unsubscribeSessionStore = sessionStore.subscribe((newState) => {
if (newState.authed && $filesystemStore && !imagesFetched) {
imagesFetched = true
// Get images from the user's public WNFS
getImagesFromWNFS()
}
})
onDestroy(() => {
unsubscribeGalleryStore()
unsubscribeSessionStore()
})
function handleSubmit() {
makeConnection();
}
export async function makeConnection(){
try{
const entryObj =
{
"@type" : "Person",
"userName" : username,
"givenName" : givenName,
"bioregion": bioregion,
"ecozone": ecozone,
"hasCredential": hasCredential,
"affiliation": affiliatedOrganizations
};
if (username == entryObj.userName){
await client.updateDocument(entryObj)
} else{
await client.addDocument(entryObj);
}
const entries2 = await client.getDocument({"graph_type":"instance","as_list":true,"type":"Person"})
console.log(entries2);
}catch(err){
console.error(err.message)
}
}
</script>
<style>
form {
display: grid;
grid-template-columns: 1fr 3fr;
}
label {
text-align: left;
}
input{
background-color: rgb(255, 255, 255);
border-radius: 4px;
}
button{
background-color: #4CAF50; /* Green */
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
</style>
<section class="overflow-hidden text-gray-700">
<div class="pt-8 p-6 md:p-8 mx-auto">
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:lg:grid-cols-6 gap-4"
>
{#each $galleryStore.selectedArea === AREAS.PRIVATE ? $galleryStore.privateImages : $galleryStore.publicImages as image}{/each}
</div>
<form on:submit|preventDefault={handleSubmit}>
<label class="label dark:text-white">
Given Name:
<input class="input text-white dark:text-black" type="text" bind:value={givenName} />
</label>
<br />
<label class="label dark:text-white">
Bioregion:
<input class="input text-white dark:text-black" type="text" bind:value={bioregion} />
</label>
<br />
<label class="label dark:text-white">
Ecozone:
<input class="input text-white dark:text-black" type="text" bind:value={ecozone} />
</label >
<br />
<label class="label dark:text-white">
Has Credential:
<input class="input text-white dark:text-black" type="text" bind:value={hasCredential} />
</label >
<br />
<label class="label dark:text-white">
Affiliated organizations:
<input class="input text-white dark:text-black" type="text" bind:value={affiliatedOrganizations}/>
</label>
<br />
<button class="bg-blue-500 text-white dark:text-black" type="submit">Submit</button>
</form>
</div>
{#if selectedImage}
<ImageModal
image={selectedImage}
isModalOpen={!!selectedImage}
on:close={clearSelectedImage}
/>
{/if}
</section>

View File

@ -0,0 +1,162 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { ipfsGatewayUrl } from '$lib/app-info';
import { galleryStore } from '$routes/gallery/stores'
import { deleteImageFromWNFS, type Gallery, type Image } from '$routes/gallery/lib/gallery'
export let image: Image
export let isModalOpen: boolean = false
let previousImage: Image | undefined
let nextImage: Image | undefined
let showPreviousArrow: boolean
let showNextArrow: boolean
let gallery: Gallery
const dispatch = createEventDispatcher()
const unsubcribe = galleryStore.subscribe(newState => (gallery = newState))
/**
* Close the modal, clear the image state vars, set `isModalOpen` to false
* and dispatch the close event to clear the image from the parent's state
*/
const handleCloseModal: () => void = () => {
image = null
previousImage = null
nextImage = null
isModalOpen = false
dispatch('close')
}
/**
* Delete an image from the user's WNFS
*/
const handleDeleteImage: () => Promise<void> = async () => {
await deleteImageFromWNFS(image.name)
handleCloseModal()
}
/**
* Set the previous and next images to be toggled to when the arrows are clicked
*/
const setCarouselState = () => {
const imageList = image.private
? gallery.privateImages
: gallery.publicImages
const currentIndex = imageList.findIndex(val => val.cid === image.cid)
previousImage =
imageList[currentIndex - 1] ?? imageList[imageList.length - 1]
nextImage = imageList[currentIndex + 1] ?? imageList[0]
showPreviousArrow = imageList.length > 1 && !!previousImage
showNextArrow = imageList.length > 1 && !!nextImage
}
/**
* Load the correct image when a user clicks the Next or Previous arrows
* @param direction
*/
const handleNextOrPrevImage: (
direction: 'next' | 'prev'
) => void = direction => {
image = direction === 'prev' ? previousImage : nextImage
setCarouselState()
}
/**
* Detect `Escape` key presses to close the modal or `ArrowRight`/`ArrowLeft`
* presses to navigate the carousel
* @param event
*/
const handleKeyDown: (event: KeyboardEvent) => void = event => {
if (event.key === 'Escape') handleCloseModal()
if (showNextArrow && event.key === 'ArrowRight')
handleNextOrPrevImage('next')
if (showPreviousArrow && event.key === 'ArrowLeft')
handleNextOrPrevImage('prev')
}
onMount(() => {
setCarouselState()
})
// Unsubscribe from galleryStore updates
onDestroy(unsubcribe)
</script>
<svelte:window on:keydown={handleKeyDown} />
{#if !!image}
<!-- bind:checked can't be set to !!image, so we need to set it to a boolean(casting image as a boolean throws a svelte error, so we're using isModalOpen) -->
<input
type="checkbox"
id={`image-modal-${image.cid}`}
class="modal-toggle"
bind:checked={isModalOpen}
/>
<label
for={`image-modal-${image.cid}`}
class="modal cursor-pointer z-50"
on:click|self={handleCloseModal}
>
<div class="modal-box relative text-center text-base-content">
<label
for={`image-modal-${image.cid}`}
class="btn btn-xs btn-circle absolute right-2 top-2"
on:click={handleCloseModal}
>
</label>
<div>
<h3 class="mb-7 text-lg break-all">{image.name}</h3>
<div class="relative">
{#if showPreviousArrow}
<button
class="absolute top-1/2 -left-[25px] -translate-y-1/2 inline-block text-center text-[40px]"
on:click={() => handleNextOrPrevImage('prev')}
>
&#8249;
</button>
{/if}
<img
class="block object-cover object-center border-2 border-base-content w-full h-full mb-4 rounded-[1rem]"
alt={`Image: ${image.name}`}
src={image.src}
/>
{#if showNextArrow}
<button
class="absolute top-1/2 -right-[25px] -translate-y-1/2 inline-block text-center text-[40px]"
on:click={() => handleNextOrPrevImage('next')}
>
&#8250;
</button>
{/if}
</div>
<div class="flex flex-col items-center justify-center">
<a
href={`https://ipfs.${ipfsGatewayUrl}/ipfs/${image.cid}/userland`}
target="_blank"
class="underline mb-4 hover:text-slate-500"
>
View on IPFS
</a>
<p class="mb-4">
Created at {new Date(image.ctime).toDateString()}
</p>
<div class="flex items-center justify-between gap-4">
<a href={image.src} download={image.name} class="btn btn-primary">
Download Image
</a>
<button class="btn btn-outline" on:click={handleDeleteImage}>
Delete Image
</button>
</div>
</div>
</div>
</div>
</label>
{/if}

View File

@ -0,0 +1,72 @@
<script lang="ts">
import TerminusClient from "@terminusdb/terminusdb-client";
import { onDestroy } from 'svelte'
import { filesystemStore, sessionStore } from '$src/stores'
import { AREAS, galleryStore } from '$routes/gallery/stores'
import { getImagesFromWNFS, type Image } from '$routes/gallery/lib/gallery'
import FileUploadCard from '$routes/gallery/components/upload/FileUploadCard.svelte'
import ImageCard from '$routes/gallery/components/imageGallery/ImageCard.svelte'
import ImageModal from '$routes/gallery/components/imageGallery/ImageModal.svelte'
/**
* Open the ImageModal and pass it the selected `image` from the gallery
* @param image
*/
let selectedImage: Image
const setSelectedImage: (image: Image) => void = image =>
(selectedImage = image)
// If galleryStore.selectedArea changes from private to public, re-run getImagesFromWNFS
let selectedArea = null
const unsubscribeGalleryStore = galleryStore.subscribe(async updatedStore => {
// Get initial selectedArea
if (!selectedArea) {
selectedArea = updatedStore.selectedArea
}
if (selectedArea !== updatedStore.selectedArea) {
selectedArea = updatedStore.selectedArea
}
})
// Once the user has been authed, fetch the images from their file system
let imagesFetched = false
const unsubscribeSessionStore = sessionStore.subscribe((newState) => {
if (newState.authed && $filesystemStore && !imagesFetched) {
imagesFetched = true
// Get images from the user's public WNFS
getImagesFromWNFS()
}
})
onDestroy(() => {
unsubscribeGalleryStore()
unsubscribeSessionStore()
})
</script>
<section class="overflow-hidden text-gray-700">
<div class="pt-8 p-6 md:p-8 mx-auto">
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:lg:grid-cols-6 gap-4"
>
<FileUploadCard />
{#each $galleryStore.selectedArea === AREAS.PRIVATE ? $galleryStore.privateImages : $galleryStore.publicImages as image}
<ImageCard {image} openModal={setSelectedImage} />
{/each}
</div>
</div>
{#if selectedImage}
<ImageModal
image={selectedImage}
isModalOpen={!!selectedImage}
on:close={clearSelectedImage}
/>
{/if}
</section>

View File

@ -0,0 +1,69 @@
<script lang="ts">
import {
getImagesFromWNFS,
uploadImageToWNFS
} from '$routes/gallery/lib/gallery'
import { addNotification } from '$lib/notifications'
/**
* Detect when a user drags a file in or out of the dropzone to change the styles
*/
let isDragging = false
const handleDragEnter: () => void = () => (isDragging = true)
const handleDragLeave: () => void = () => (isDragging = false)
/**
* Process files being dropped in the drop zone and ensure they are images
* @param event
*/
const handleDrop: (event: DragEvent) => Promise<void> = async event => {
// Prevent default behavior (Prevent file from being opened)
event.preventDefault()
const files = Array.from(event.dataTransfer.items)
// Iterate over the dropped files and upload them to WNFS
await Promise.all(
files.map(async item => {
if (item.kind === 'file') {
const file: File = item.getAsFile()
// If the dropped files aren't images, we don't want them!
if (!file.type.match('image/*')) {
addNotification('Please upload images only', 'error')
console.error('Please upload images only')
} else {
await uploadImageToWNFS(file)
}
}
})
)
// Refetch images and update galleryStore
await getImagesFromWNFS()
// Disable isDragging state
isDragging = false
}
/**
* This is needed to prevent the default behaviour of the file opening in browser
* when it is dropped
* @param event
*/
const handleDragOver: (event: DragEvent) => void = event =>
event.preventDefault()
</script>
<label
on:drop={handleDrop}
on:dragover={handleDragOver}
on:dragenter={handleDragEnter}
on:dragleave={handleDragLeave}
for="dropzone-file"
class="block w-full min-h-[calc(100vh-190px)] rounded-lg border-2 border-solid border-base-content transition ease-in cursor-pointer {isDragging
? 'border-dashed !border-orange-700 bg-orange-50'
: ''}"
>
<slot />
</label>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import { galleryStore } from '$routes/gallery/stores'
import { handleFileInput } from '$routes/gallery/lib/gallery'
import FileUploadIcon from '$routes/gallery/components/icons/FileUploadIcon.svelte'
// Handle files uploaded directly through the file input
let files: FileList
$: if (files) {
handleFileInput(files)
}
</script>
<label
for="upload-file"
class="group btn !p-0 !h-auto flex flex-col justify-center items-center aspect-[22/23] object-cover rounded-lg shadow-orange hover:border-neutral-50 overflow-hidden transition-colors ease-in bg-base-100 border-2 box-content border-neutral cursor-pointer text-neutral bg-gradient-to-r from-orange-600 to-orange-300"
>
{#if $galleryStore.loading}
<div class="flex justify-center items-center p-12">
<div
class="loader ease-linear rounded-full border-4 border-t-4 border-t-orange-300 border-neutral h-16 w-16 animate-spin"
/>
</div>
{:else}
<div class="flex flex-col justify-center items-center pt-5 pb-6">
<FileUploadIcon />
<p class="mt-4 mb-2 text-sm">
<span class="font-bold text-sm">Upload a photo</span>
</p>
<p class="text-xxs">SVG, PNG, JPG or GIF</p>
</div>
<input
bind:files
id="upload-file"
type="file"
multiple
accept="image/*"
class="hidden"
/>
{/if}
</label>

View File

@ -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"
}]
}

View File

@ -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<void> = 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<void> = 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<void> = 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<void> = async files => {
await Promise.all(
Array.from(files).map(async file => {
await uploadImageToWNFS(file)
})
)
// Refetch images and update galleryStore
await getImagesFromWNFS()
}

View File

@ -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": ".",