start visualization with cytoscape
This commit is contained in:
parent
77ea389b5c
commit
c15f5a0da7
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
|
"cytoscape": "^3.23.0",
|
||||||
"qrcode-svg": "^1.1.0",
|
"qrcode-svg": "^1.1.0",
|
||||||
"uint8arrays": "^3.1.0",
|
"uint8arrays": "^3.1.0",
|
||||||
"webnative": "^0.34.1"
|
"webnative": "^0.34.1"
|
||||||
|
|
@ -18,6 +19,7 @@
|
||||||
"@sveltejs/kit": "1.0.0-next.489",
|
"@sveltejs/kit": "1.0.0-next.489",
|
||||||
"@tailwindcss/typography": "^0.5.2",
|
"@tailwindcss/typography": "^0.5.2",
|
||||||
"@terminusdb/terminusdb-client": "^10.0.25",
|
"@terminusdb/terminusdb-client": "^10.0.25",
|
||||||
|
"@types/cytoscape": "^3.19.9",
|
||||||
"@types/qrcode-svg": "^1.1.1",
|
"@types/qrcode-svg": "^1.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||||
"@typescript-eslint/parser": "^4.33.0",
|
"@typescript-eslint/parser": "^4.33.0",
|
||||||
|
|
@ -1663,6 +1665,12 @@
|
||||||
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
|
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||||
|
|
@ -3183,6 +3191,18 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/daisyui": {
|
||||||
"version": "2.19.0",
|
"version": "2.19.0",
|
||||||
"integrity": "sha512-lLOz4cHm3xpm0AfdFojDFrhiDu4hZTdEbcVJri+KzQn1HvxmZS4STVujn8tV4RQXjchGdnIsXFqxd6F7rVZBiA==",
|
"integrity": "sha512-lLOz4cHm3xpm0AfdFojDFrhiDu4hZTdEbcVJri+KzQn1HvxmZS4STVujn8tV4RQXjchGdnIsXFqxd6F7rVZBiA==",
|
||||||
|
|
@ -4572,6 +4592,11 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/html-escaper": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
|
|
@ -10250,6 +10275,12 @@
|
||||||
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
|
"integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==",
|
||||||
"dev": true
|
"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": {
|
"@types/graceful-fs": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||||
|
|
@ -11298,6 +11329,15 @@
|
||||||
"array-find-index": "^1.0.1"
|
"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": {
|
"daisyui": {
|
||||||
"version": "2.19.0",
|
"version": "2.19.0",
|
||||||
"integrity": "sha512-lLOz4cHm3xpm0AfdFojDFrhiDu4hZTdEbcVJri+KzQn1HvxmZS4STVujn8tV4RQXjchGdnIsXFqxd6F7rVZBiA==",
|
"integrity": "sha512-lLOz4cHm3xpm0AfdFojDFrhiDu4hZTdEbcVJri+KzQn1HvxmZS4STVujn8tV4RQXjchGdnIsXFqxd6F7rVZBiA==",
|
||||||
|
|
@ -12221,6 +12261,11 @@
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
"dev": true
|
"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": {
|
"html-escaper": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,11 @@
|
||||||
"format": "prettier --write --plugin-search-dir=. ."
|
"format": "prettier --write --plugin-search-dir=. ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@terminusdb/terminusdb-client": "^10.0.25",
|
|
||||||
"@sveltejs/adapter-static": "1.0.0-next.43",
|
"@sveltejs/adapter-static": "1.0.0-next.43",
|
||||||
"@sveltejs/kit": "1.0.0-next.489",
|
"@sveltejs/kit": "1.0.0-next.489",
|
||||||
"@tailwindcss/typography": "^0.5.2",
|
"@tailwindcss/typography": "^0.5.2",
|
||||||
|
"@terminusdb/terminusdb-client": "^10.0.25",
|
||||||
|
"@types/cytoscape": "^3.19.9",
|
||||||
"@types/qrcode-svg": "^1.1.1",
|
"@types/qrcode-svg": "^1.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||||
"@typescript-eslint/parser": "^4.33.0",
|
"@typescript-eslint/parser": "^4.33.0",
|
||||||
|
|
@ -55,6 +56,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
|
"cytoscape": "^3.23.0",
|
||||||
"qrcode-svg": "^1.1.0",
|
"qrcode-svg": "^1.1.0",
|
||||||
"uint8arrays": "^3.1.0",
|
"uint8arrays": "^3.1.0",
|
||||||
"webnative": "^0.34.1"
|
"webnative": "^0.34.1"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,11 @@
|
||||||
href: '/gallery/',
|
href: '/gallery/',
|
||||||
icon: PhotoGallery
|
icon: PhotoGallery
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Explore',
|
||||||
|
href: '/explore/',
|
||||||
|
icon: PhotoGallery
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'About This Template',
|
label: 'About This Template',
|
||||||
href: '/about/',
|
href: '/about/',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Explore from '$components/explore/Explore.svelte'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Explore />
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 |
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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')}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</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')}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</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}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"moduleResolution": "node",
|
"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"],
|
"lib": ["es2020"],
|
||||||
"target": "es2019",
|
"target": "es2019",
|
||||||
/**
|
/**
|
||||||
|
|
@ -16,7 +22,6 @@
|
||||||
enable source maps by default.
|
enable source maps by default.
|
||||||
*/
|
*/
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue