Compare commits

...

170 Commits
0.3.3 ... main

Author SHA1 Message Date
2e19d5fdeb Update .env 2024-06-08 20:44:20 -04:00
63f31e0f4c Update server.ts 2024-06-08 20:08:25 -04:00
f75d889ee8 Delete page.tsx 2024-06-08 19:51:44 -04:00
8877362484 Update server.ts 2024-06-08 19:50:08 -04:00
082c161959 Update page.tsx 2024-06-08 19:45:31 -04:00
5d96002a2e fix 2024-06-08 19:43:26 -04:00
65ddd704f0 fix 2024-06-08 19:34:52 -04:00
b12d095e3e Update page.tsx 2024-06-08 19:15:21 -04:00
581ceb028a fix 2024-06-08 19:14:31 -04:00
7ac84fc0cf Update page.tsx 2024-06-08 19:13:54 -04:00
164aec1c51 fix: hooked up tiers to galleries page 2024-06-08 19:11:32 -04:00
420e029d66 fix: gallery admin pages 2024-06-08 19:01:55 -04:00
d94f3b825b fix: gallery admin page 2024-06-08 18:53:10 -04:00
7635178e3c fix: gallery pages 2024-06-08 18:45:28 -04:00
a9968ccb03 fix: added ui for managing theme and tiers 2024-06-08 00:26:23 -04:00
4d86b8aeed Update page.tsx 2024-06-04 23:07:20 -04:00
19bee6998a fix: added tier pages 2024-06-04 22:59:02 -04:00
fcf83c6435 Merge branch 'main' of https://github.com/D4M13N-D3V/neroshitron 2024-06-04 00:27:25 -04:00
32ba288463 Update diagrams.drawio 2024-06-04 00:27:23 -04:00
334db37d18
Update README.md 2024-06-04 00:25:36 -04:00
1112191b61
Update README.md 2024-06-04 00:24:08 -04:00
52b7771994
Update README.md 2024-06-04 00:23:21 -04:00
9e76f9315b
Update README.md 2024-06-03 23:50:58 -04:00
c9bf8e8431 Update navigation_bar.tsx 2024-06-03 22:25:21 -04:00
691292becc fix:reactive pages 2024-06-03 22:21:19 -04:00
e8fa3d2ce0 Update page.tsx 2024-06-03 21:45:50 -04:00
fd76220269 fix: adding duplicates 2024-06-03 21:37:05 -04:00
e9e4f25520 Update search_input.tsx 2024-06-03 21:24:42 -04:00
234cc989f1 fixed: nsfw button 2024-06-03 21:23:08 -04:00
e6a81a3bdf fix: tag format 2024-06-03 21:02:48 -04:00
96aeca6242 Update page.tsx 2024-06-03 02:39:11 -04:00
a6d050c35f Update page.tsx 2024-06-03 02:38:20 -04:00
f790a03c27 fix 2024-06-03 02:29:09 -04:00
6f5bf90a0b fix 2024-06-03 02:25:25 -04:00
e5daff376a Update search.tsx 2024-06-03 02:15:13 -04:00
a0a38389a6 Update page.tsx 2024-06-03 02:14:45 -04:00
85c066baec Update page.tsx 2024-06-03 02:13:38 -04:00
9c6bdddd22 Update page.tsx 2024-06-03 02:12:25 -04:00
87ba2e032d fix 2024-06-03 02:10:39 -04:00
28414630a4 feat: gallery management pages 2024-06-03 02:00:05 -04:00
534cbf2957 fix 2024-06-02 23:37:41 -04:00
481b4f698c fix: 2024-06-02 23:14:30 -04:00
172a3015af Merge branch 'main' of https://github.com/D4M13N-D3V/neroshitron 2024-06-02 22:58:53 -04:00
6564be1a60 Update search_input.tsx 2024-06-02 22:58:49 -04:00
fdcaccfbd6
23 replace recenter with back (#37)
* fix:23

* fix

* fix: girl bg

* Update search_input.tsx

* Update search_input.tsx
2024-06-02 22:57:20 -04:00
6541bd94fe
23 replace recenter with back (#36)
* fix:23

* fix

* fix: girl bg

* Update search_input.tsx
2024-06-02 22:52:44 -04:00
82fdd61170
fix:23 (#35) 2024-06-02 20:25:29 -04:00
caca715cc0
fix:22 (#34) 2024-06-02 20:21:12 -04:00
74335ff6cf Update seed.sql 2024-06-02 19:51:45 -04:00
3206a19f7a Delete route.ts 2024-06-02 19:31:51 -04:00
e48d3ebafb fix;admin 2024-06-02 19:26:12 -04:00
168c835a23 fix: animations 2024-06-02 17:58:27 -04:00
0127d00021 fix: locked down navbar options for admin pages and added rls policies 2024-06-02 17:50:08 -04:00
02872252b5 feat: tags admin ui 2024-06-02 17:35:10 -04:00
b6736651c5 fix 2024-06-02 07:54:45 -04:00
78a25ab398 Update right_hand_layout_image.tsx 2024-06-02 07:51:11 -04:00
58d384a3fc fix: shadows 2024-06-02 07:49:41 -04:00
76a09c8e8c fix: nav 2024-06-02 07:43:33 -04:00
73836c5893 fix: styling for tags search 2024-06-02 07:17:14 -04:00
4c972778cd fix: moved anime girl to layout 2024-06-02 06:12:04 -04:00
795fc0f5f7 fix
const [scrollPosition, setScrollPosition] = useState(0);
  const [color, setColor] = useState('black');

  useEffect(() => {
  }, []);
2024-06-02 05:57:18 -04:00
e4dde90da4 fix 2024-06-02 04:56:09 -04:00
fa6bf10249 Update page.tsx 2024-06-02 04:52:32 -04:00
cd5d0bf304 Update page.tsx 2024-06-02 04:50:23 -04:00
4a3c7023ba Update page.tsx 2024-06-02 04:46:42 -04:00
ea3a14a6e4 fix; delete button 2024-06-02 04:46:21 -04:00
451bdd1106 Update page.tsx 2024-06-02 04:42:34 -04:00
2b21c2feff Update page.tsx 2024-06-02 04:37:59 -04:00
02e85c54fc Update page.tsx 2024-06-02 04:35:15 -04:00
ac4007a82e Update page.tsx 2024-06-02 04:31:38 -04:00
e584a43145 Update navigation_bar.tsx 2024-06-02 04:28:52 -04:00
b232be9377 fix: tags 2024-06-02 04:25:36 -04:00
46a20bbd7e Update page.tsx 2024-06-02 03:52:45 -04:00
894fb58c29 fix: create page 2024-06-02 03:51:53 -04:00
2c59660566 feat: create page 2024-06-02 03:48:21 -04:00
666e377cdb Update search_input.tsx 2024-06-02 03:17:05 -04:00
da6fc56b86 fix 2024-06-02 03:15:59 -04:00
5daa585a9e Merge branch 'main' of https://github.com/D4M13N-D3V/neroshitron 2024-06-02 03:09:02 -04:00
d33f36adaf fix 2024-06-02 03:08:55 -04:00
e03816928a
fix:images in gallery (#18) 2024-06-02 02:49:55 -04:00
de08c2ddd0 Update navigation_bar.tsx 2024-06-02 02:20:37 -04:00
9de286f920 fix: added a thing telling user where tags are 2024-06-02 02:13:36 -04:00
45d3b459d8 Update search_input.tsx 2024-06-02 02:11:56 -04:00
712e1ce5cd Update search_input.tsx 2024-06-02 02:07:20 -04:00
912fa157e2 Update search_input.tsx 2024-06-02 02:04:58 -04:00
394163198c Update gallery.tsx 2024-06-02 01:53:31 -04:00
f74d7a8fa9 Update search_input.tsx 2024-06-02 01:49:24 -04:00
cfe47be428 fix: neroshi tag isnt pushed 2024-06-02 01:37:52 -04:00
bd334bcbea fix: first tag 2024-06-02 01:37:03 -04:00
54ec36cebc fix: nsfw button 2024-06-02 01:32:37 -04:00
f175b31976 fix: css themiing 2024-06-02 01:24:14 -04:00
9b5c3d44c5 chore: formatting 2024-06-02 00:36:02 -04:00
97f2a31951 Update gallery_thumbnail.tsx 2024-06-02 00:31:19 -04:00
4c564df94a Update search_input.tsx 2024-06-02 00:25:42 -04:00
2edf752b05 Update search_input.tsx 2024-06-02 00:20:03 -04:00
d7f3330dc2 Merge branch 'main' of https://github.com/D4M13N-D3V/neroshitron 2024-06-02 00:19:43 -04:00
afe4076874 Update search_input.tsx 2024-06-02 00:19:36 -04:00
f86c618b1d
fix: remove scrollbar (#8)
* Update .gitignore

* fix: added a badge for number of tags selected

* Update tag_selector.tsx

* tag search

* fix: search styling
2024-06-02 00:15:21 -04:00
7fec3450d9
fix:add badge showing how many tags selected (#7)
* Update .gitignore

* fix: added a badge for number of tags selected
2024-06-01 16:36:06 -04:00
41c90c2a58
fix: clicking tag breaks tag ui (#6) 2024-06-01 16:30:05 -04:00
bef53cadce
fix: gallery not opening (#5) 2024-06-01 16:29:58 -04:00
f30d2c5a2a
feat: new shit (#4)
* fix: navigation bar code is minimized, started seperating out components and refactoring them

* feat: search components

* Update search.tsx

* fix: autofocus

* fix: tags and search

* Update page_old.tsx

* Update galleries.tsx

* Update tag_selector.tsx

* Update tag_selector.tsx
2024-06-01 06:39:05 -04:00
f2bd76487e
feat: tags
* fix: navigation bar code is minimized, started seperating out components and refactoring them

* feat: search components

* Update search.tsx

* fix: autofocus

* fix: tags and search
2024-06-01 06:21:59 -04:00
92c2317af1 Update NavigationBar.tsx 2024-05-28 00:36:27 -04:00
0b390b7658 Update NavigationBar.tsx 2024-05-28 00:34:14 -04:00
8a994123af Update _middleware.tsx 2024-05-28 00:28:28 -04:00
dcfe7c5676 fix:header 2024-05-28 00:28:03 -04:00
21fa376190 fix: navbar highlight 2024-05-28 00:20:42 -04:00
23137d4e04 Update NavigationBar.tsx 2024-05-28 00:11:45 -04:00
b99f3fa256 Update NavigationBar.tsx 2024-05-28 00:10:45 -04:00
2c036794ff Update NavigationBar.tsx 2024-05-28 00:09:23 -04:00
ee1272b520 Update NavigationBar.tsx 2024-05-28 00:08:42 -04:00
2cccc65a9a Update NavigationBar.tsx 2024-05-28 00:08:32 -04:00
e17aba510c Update NavigationBar.tsx 2024-05-28 00:07:38 -04:00
381009d5c9 chore :analytics 2024-05-28 00:04:45 -04:00
401e3d41dc Update NavigationBar.tsx 2024-05-28 00:02:55 -04:00
82728cbc97 Update NavigationBar.tsx 2024-05-27 23:58:58 -04:00
de0a5a8e73 Update NavigationBar.tsx 2024-05-27 23:55:21 -04:00
057c20cd21 Update NavigationBar.tsx 2024-05-27 23:54:39 -04:00
b2f700280a chore: installed speed insights 2024-05-27 23:47:49 -04:00
e8161fc226 Update NavigationBar.tsx 2024-05-27 23:46:24 -04:00
10f8e5d289 Update NavigationBar.tsx 2024-05-27 23:28:16 -04:00
54a771b47f Update page.tsx 2024-05-27 23:26:41 -04:00
e82305954e Update page.tsx 2024-05-27 23:24:59 -04:00
fe09172881 Update page.tsx 2024-05-27 23:24:18 -04:00
caac354273 Update page.tsx 2024-05-27 23:19:30 -04:00
ea21548c68 Update layout.tsx 2024-05-27 23:16:08 -04:00
eb8df7bed7 Update NavigationBar.tsx 2024-05-27 21:45:17 -04:00
4b0060e5dd cleanup 2024-05-27 21:38:21 -04:00
08ce73e64e chrore ; update 2024-05-27 20:53:24 -04:00
af0dd625b4 feat: nsfw toggle 2024-05-27 20:40:23 -04:00
43432a8bcd chore: more feel on gallery tiles 2024-05-27 20:19:20 -04:00
5e5678c280 fix: animate the search bar coming out 2024-05-27 20:10:03 -04:00
c0a382e4a7 Merge branch 'main' of https://github.com/D4M13N-D3V/neroshitron 2024-05-27 20:04:09 -04:00
ab59264d84 chore:diagrams 2024-05-27 20:04:05 -04:00
ea68c28b2e
Update README.md 2024-05-27 20:02:22 -04:00
6cd71b64e8 fix:bluring 2024-05-27 17:42:06 -04:00
b93f3945bf fix; skeleton for tags/search 2024-05-27 17:37:19 -04:00
53fcbb76f6 chore:seed data 2024-05-27 17:37:02 -04:00
8ad20079de fix: not being able to scroll to bottom 2024-05-27 17:18:44 -04:00
10f36e54ff feat: image skeletons 2024-05-27 17:09:49 -04:00
e930b2ce73 fix: resizing for gallery 2024-05-27 17:02:25 -04:00
b5a796d3ff fix:place holder for search 2024-05-27 17:02:17 -04:00
abab8c9336 fix:navbar icons 2024-05-27 16:45:37 -04:00
82399d1bc3 fix:gallery page resizing 2024-05-27 16:38:34 -04:00
cb37b333b0 Update route.ts 2024-05-27 11:40:00 -04:00
adfab3585b fix :nsfw not bluring 2024-05-27 11:38:38 -04:00
052bc21604 fix: wrong pic count 2024-05-27 11:36:56 -04:00
1647474a74 Update README.md 2024-05-27 11:29:29 -04:00
cf7e485f4e Update seed.sql 2024-05-27 11:28:10 -04:00
ab0198df01 Update seed.sql 2024-05-27 11:18:03 -04:00
a48488a128 fix:supabase+tags+changed data models+seeded data 2024-05-27 11:17:56 -04:00
82efd1f10e Update README.md 2024-05-27 09:53:06 -04:00
70ea1d8a9c chore :readme default env 2024-05-27 09:52:26 -04:00
97eba7457d Update README.md 2024-05-27 09:42:06 -04:00
8eedebfee2 Update README.md 2024-05-27 09:41:33 -04:00
9f205dd034 feat:setup supabase cli 2024-05-27 09:40:21 -04:00
f7cef69b5b fix: tiles no longer get cut off 2024-05-27 08:19:53 -04:00
090630c507 feat: blurs based on tier access 2024-05-27 00:14:31 -04:00
6af5a0ee5d fix: icons not updating on search 2024-05-26 22:09:05 -04:00
323ba05fda fix:recenter 2024-05-26 21:57:10 -04:00
8f41b42cf4 Update gallery.tsx 2024-05-26 21:27:26 -04:00
58a10bcc41 Update gallery.tsx 2024-05-26 21:26:24 -04:00
b1bd8ab67e fix:center iamge 2024-05-26 21:16:47 -04:00
f53ea97c3e Update page.tsx 2024-05-26 21:12:39 -04:00
21307711e6 Merge branch 'main' of https://github.com/D4M13N-D3V/neroshitron 2024-05-26 21:12:10 -04:00
7780a3a0d1 Update page.tsx 2024-05-26 21:12:09 -04:00
7bf385f8c0
Update README.md 2024-05-26 20:23:58 -04:00
867af6ac1d Update README.md 2024-05-26 20:22:11 -04:00
0203f848b1 Update .gitignore 2024-05-26 20:21:42 -04:00
79 changed files with 5965 additions and 2209 deletions

View File

@ -1,3 +1,2 @@
NEXT_APPWRITE_KEY=<API_KEY> NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
NEXT_PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1 NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
NEXT_PUBLIC_APPWRITE_PROJECT=<PROJECT_ID>

5
.env
View File

@ -1,3 +1,2 @@
NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000 NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
NEXT_PUBLIC_

2
.env.production Normal file
View File

@ -0,0 +1,2 @@
NEXT_PUBLIC_SUPABASE_URL=https://cgnmpfjcbhtgdcizgfnc.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNnbm1wZmpjYmh0Z2RjaXpnZm5jIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTY4NTkyNDIsImV4cCI6MjAzMjQzNTI0Mn0.oGf2jp-Twr1qP7GQI4D87iu5EWV6UhL51OEaCLvSMxk

5
.gitignore vendored
View File

@ -22,3 +22,8 @@ yarn-error.log*
/coverage /coverage
/supabase/volumes/db/data /supabase/volumes/db/data
/supabase/volumes/storage /supabase/volumes/storage
supabase.zip
.idea/copilot/chatSessions/00000000000.xd
.idea/copilot/chatSessions/blobs/version
.idea/copilot/chatSessions/xd.lck
.idea/workspace.xml

5
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
page.tsx

17
.idea/aws.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="accountSettings">
<option name="activeProfile" value="profile:default" />
<option name="activeRegion" value="us-east-1" />
<option name="recentlyUsedProfiles">
<list>
<option value="profile:default" />
</list>
</option>
<option name="recentlyUsedRegions">
<list>
<option value="us-east-1" />
</list>
</option>
</component>
</project>

4
.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

6
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_22" default="true" project-jdk-name="openjdk-22" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/neroshitron.iml" filepath="$PROJECT_DIR$/.idea/neroshitron.iml" />
</modules>
</component>
</project>

11
.idea/neroshitron.iml generated Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.idea/copilot/chatSessions" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["denoland.vscode-deno"]
}

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"deno.enablePaths": [
"supabase/functions"
],
"deno.lint": true,
"deno.unstable": true,
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}

View File

@ -1,80 +1,54 @@
# Neroshitron # Neroshitron
![image](https://github.com/D4M13N-D3V/neroshitron/assets/13697702/e4a9e11b-0e52-42e0-ad9a-821a81e92e90) ![image](https://github.com/D4M13N-D3V/neroshitron/assets/13697702/e4a9e11b-0e52-42e0-ad9a-821a81e92e90)
![image](https://github.com/D4M13N-D3V/neroshitron/assets/13697702/78e009be-caa0-4ae6-9c06-90dde2ab4389)
# Documentation For Technical Stack # Documentation For Technical Stack
- https://nextjs.org/docs - https://nextjs.org/docs
- https://supabase.com/docs/guides/self-hosting/docker - https://supabase.com/docs/
- https://www.docker.com/products/docker-desktop/ - https://owncast.online/docs/
- https://docs.npmjs.com/downloading-and-installing-node-js-and-npm - https://docs.docker.com/engine/install/
# Running Backend # Running Backend
You will need docker installed.
- https://docs.docker.com/engine/install/
You will need supabase CLI.
- https://docs.docker.com/engine/install/
You need npm and nodejs installed. See documentation at start of document.
- https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
1) Open your terminal and navigate to the root of the git repository. 1) Open your terminal and navigate to the root of the git repository.
2) Make sure that docker and docker compose are installed. 2) Make sure that docker and docker compose are installed.
3) Run `docker-compose --env-file ./docker.env up` which will start up OwnCast, AppWrite, and the UI. 3) Run `docker-compose --env-file ./docker.env up` which will start up OwnCast.
4) Run `supabase start`
5) Open your terminal and navigate to the root folder of the git repository.
6) Run the command `npm update`.
7) Once the depedencies are pulled and installed you can run the command `npm run dev` to run the application in development mode.
8) Open http://localhost:3000/
## MailDev
http://localhost:1080
### Updating the database/Seeding data
Run `supabase db reset`. This will wipe data.
https://supabase.com/docs/guides/cli/local-development?queryGroups=access-method&access-method=kong#database-migrations
** Once the data is seeded you will need to go to the galleries bucket and add images to the folders that exist in it for the seeded galleries. **
## inbucket
http://localhost:54324su/monitor
This is where all mail being sent shows up from the application for developers. This is where all mail being sent shows up from the application for developers.
## OwnCast ## OwnCast
http://localhost:8080/ http://localhost:8080/
Configuration is done through the Owncast administration page located on your server under /admin. The login username is admin and the password is your stream key, the default being abc123. Configuration is done through the Owncast administration page located on your server under /admin. The login username is admin and the password is your stream key, the default being abc123.
## Supabase # User Flow Diagram
http://localhost:8000/ ![image](https://github.com/D4M13N-D3V/neroshitron/assets/13697702/57379445-8bd5-4a7e-8a15-7fa0b3ae42dc)
You will need to register and sign up, the first account on the appwrite instance will be the admin account.çConfiguration is done through the Owncast administration page located on your server under /admin. The login username is admin and the password is your stream key, the default being abc123.
# Running UI # Database Diagram
You need npm and nodejs installed. See documentation at start of document. ![image](https://github.com/D4M13N-D3V/neroshitron/assets/13697702/b3387b77-d8c6-41c4-9fe8-d52dadb6bc5e)
1) Open your terminal and navigate to the root folder of the git repository.
2) Run the command `npm update`.
3) Once the depedencies are pulled and installed you can run the command `npm run dev` to run the application in development mode.
4) Open http://localhost:3000/
# React Components
## Gallery Component
The `Gallery` component is a React component used to display a gallery of images. It fetches images from an API and displays them in a Masonry layout.
### Props
- `id` (number): The ID of the gallery to fetch images from.
- `closeMenu` (function): A function to close the menu.
### State
- `isSingle` (boolean): A state to check if only a single image is to be displayed.
- `loaded` (object): A state to keep track of loaded images.
- `selectedImage` (string | null): A state to keep track of the selected image.
- `images` (string[]): A state to store the fetched images.
- `galleryId` (number): A state to store the gallery ID.
### Functions
- `getData`: An async function to fetch images from the API.
- `generateRandomString`: A function to generate a random string of a given length.
- `handleDownload`: A function to handle the download of an image.
### Usage
The example below will load the images of the the gallery with an ID of `58201557-b392-471e-ac55-dcf6171cd18d` and will call the function for `setIsOpen(false)` when the back button is clicked.
```tsx
<Gallery id={"58201557-b392-471e-ac55-dcf6171cd18d"} closeMenu={() => setIsOpen(false)}></Gallery>
```
## GalleryThumbnail Component
The `GalleryThumbnail` component is a React component used to display a thumbnail of a gallery. It fetches the thumbnail image from an API and displays it. When clicked, it triggers a callback function with the gallery ID.
### Props
- `id` (string): The ID of the gallery to fetch the thumbnail for.
- `onSelect` (function): A function to be called when the thumbnail is clicked. The gallery ID is passed as an argument.
- `title` (string) : The name of the gallery.
- `subscription` (subscription) : The name of the subscription required for the gallery.
### State
- `galleryId` (string): A state to store the gallery ID.
- `thumbnailUrl` (string): A state to store the fetched thumbnail URL.
- `isLoading` (boolean): A state to keep track of the loading status.
### Functions
- `openGallery`: A function to call the `onSelect` prop with the gallery ID.
- `getData`: An async function to fetch the thumbnail from the API.
### Usage
This will render a thumbnail for the gallery with the ID of "1". When the thumbnail is clicked, it will log the gallery ID to the console.
```tsx
<GalleryThumbnail id="1" onSelect={(id) => console.log(id)} />
```

4
app/_middleware.tsx Normal file
View File

@ -0,0 +1,4 @@
import { middleware } from './middleware/path-header'
export default middleware

41
app/admin/page.tsx Normal file
View File

@ -0,0 +1,41 @@
"use client";
import { createClient } from "@/utils/supabase/client";
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
function PageComponent() {
const supabase = createClient();
const getData = async () => {
}
useEffect(() => {
getData();
}, []);
const router = useRouter();
return (
<div className="w-full text-white flex justify-center items-center animate-in">
<div className="w-2/3 lg:w-1/3 rounded-md bg-primary p-8 mt-32 shadow-lg opacity-90 backdrop-blur-lg">
<div className="w-full flex justify-center">
<span className="text-2xl pb-4">System Settings</span>
</div>
<div className="flex justify-center ">
<button onClick={()=>{ router.push("/admin/tiers") }} className="bg-secondary hover:bg-secondary-dark rounded p-2 mr-1 w-full">Tiers Management</button>
<button onClick={()=>{ router.push("/gallery/admin") }} className="ml-1 bg-secondary hover:bg-secondary-dark rounded p-2 w-full">Gallery Management</button>
</div>
<div className="flex justify-center pt-2">
<button onClick={()=>{ router.push("/admin/theme") }} className="mr-1 bg-secondary hover:bg-secondary-dark rounded p-2 mr-1 w-full">Theme Settings</button>
<button className="bg-secondary hover:bg-secondary-dark rounded p-2 ml-1 w-full">User Management</button>
</div>
<div className="flex justify-center pt-2">
<button className="bg-secondary hover:bg-secondary-dark rounded p-2 w-full mr-1">Payment/Payout Settings</button>
<button className="bg-secondary hover:bg-secondary-dark rounded p-2 w-full ml-1">Comissions Settings</button>
</div>
</div>
</div>
);
}
export default PageComponent;

474
app/admin/theme/page.tsx Normal file
View File

@ -0,0 +1,474 @@
"use client";
import { createClient } from "@/utils/supabase/client";
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
function PageComponent() {
const router = useRouter();
const supabase = createClient();
const [primary, setPrimary] = useState('#201240');
const [primaryLight, setPrimaryLight] = useState('#403260');
const [primaryDark, setPrimaryDark] = useState('#100120');
const [secondary, setSecondary] = useState('#4F3D70');
const [secondaryLight, setSecondaryLight] = useState('#6F5D90');
const [secondaryDark, setSecondaryDark] = useState('#2F1D50');
const [error, setError] = useState('#862117');
const [errorLight, setErrorLight] = useState('#C44C4C');
const [errorDark, setErrorDark] = useState('#5C0D0D');
const [changed, setChanged] = useState(false);
const [success, setSuccess] = useState('#00C9A6');
const [successLight, setSuccessLight] = useState('#20E9C6');
const [successDark, setSuccessDark] = useState('#00A986');
const [warning, setWarning] = useState('#E17558');
const [warningLight, setWarningLight] = useState('#E39578');
const [warningDark, setWarningDark] = useState('#C15538');
const [info, setInfo] = useState('#222140');
const [infoLight, setInfoLight] = useState('#424260');
const [infoDark, setInfoDark] = useState('#020120');
const [saving, setSaving] = useState(false);
const getData = async () => {
var primaryConfig = await getColorConfig('primary');
setPrimary(primaryConfig.value);
var primaryLightConfig = await getColorConfig('primary-light');
setPrimaryLight(primaryLightConfig.value);
var primaryDarkConfig = await getColorConfig('primary-dark');
setPrimaryDark(primaryDarkConfig.value);
var secondaryConfig = await getColorConfig('secondary');
setSecondary(secondaryConfig.value);
var secondaryLightConfig = await getColorConfig('secondary-light');
setSecondaryLight(secondaryLightConfig.value);
var secondaryDarkConfig = await getColorConfig('secondary-dark');
setSecondaryDark(secondaryDarkConfig.value);
var errorConfig = await getColorConfig('error');
setError(errorConfig.value);
var errorLightConfig = await getColorConfig('error-light');
setErrorLight(errorLightConfig.value);
var errorDarkConfig = await getColorConfig('error-dark');
setErrorDark(errorDarkConfig.value);
var successConfig = await getColorConfig('success');
setSuccess(successConfig.value);
var successLightConfig = await getColorConfig('success-light');
setSuccessLight(successLightConfig.value);
var successDarkConfig = await getColorConfig('success-dark');
setSuccessDark(successDarkConfig.value);
var warningConfig = await getColorConfig('warning');
setWarning(warningConfig.value);
var warningLightConfig = await getColorConfig('warning-light');
setWarningLight(warningLightConfig.value);
var warningDarkConfig = await getColorConfig('warning-dark');
setWarningDark(warningDarkConfig.value);
var infoConfig = await getColorConfig('info');
setInfo(infoConfig.value);
var infoLightConfig = await getColorConfig('info-light');
setInfoLight(infoLightConfig.value);
var infoDarkConfig = await getColorConfig('info-dark');
setInfoDark(infoDarkConfig.value);
}
const getColorConfig = async (name: string) => {
try {
const response = await fetch(`/api/admin/interface-configs?name=${name}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to call GET request');
}
const data = await response.json();
return data;
} catch (error) {
console.error(error);
throw new Error('Failed to call GET request');
}
}
const setColorConfig = async (name: string, value:string) => {
try {
const response = await fetch(`/api/admin/interface-configs`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, value }),
});
if (!response.ok) {
throw new Error('Failed to call GET request');
}
const data = await response.json();
return data;
} catch (error) {
console.error(error);
throw new Error('Failed to call GET request');
}
}
const save = async () => {
setSaving(true);
await Promise.all([
setColorConfig('primary', primary),
setColorConfig('primary-light', primaryLight),
setColorConfig('primary-dark', primaryDark),
setColorConfig('secondary', secondary),
setColorConfig('secondary-light', secondaryLight),
setColorConfig('secondary-dark', secondaryDark),
setColorConfig('error', error),
setColorConfig('error-light', errorLight),
setColorConfig('error-dark', errorDark),
setColorConfig('success', success),
setColorConfig('success-light', successLight),
setColorConfig('success-dark', successDark),
setColorConfig('warning', warning),
setColorConfig('warning-light', warningLight),
setColorConfig('warning-dark', warningDark),
setColorConfig('info', info),
setColorConfig('info-light', infoLight),
setColorConfig('info-dark', infoDark)
]);
window.location.reload();
}
useEffect(() => {
getData();
}, []);
const colorChange = async ()=>{
setChanged(true)
document.documentElement.style.setProperty(`--color-primary`, primary);
document.documentElement.style.setProperty(`--color-primary-light`, primaryLight);
document.documentElement.style.setProperty(`--color-primary-dark`, primaryDark);
document.documentElement.style.setProperty(`--color-secondary`, secondary);
document.documentElement.style.setProperty(`--color-secondary-light`, secondaryLight);
document.documentElement.style.setProperty(`--color-secondary-dark`, secondaryDark);
document.documentElement.style.setProperty(`--color-error`, error);
document.documentElement.style.setProperty(`--color-error-light`, errorLight);
document.documentElement.style.setProperty(`--color-error-dark`, errorDark);
document.documentElement.style.setProperty(`--color-success`, success);
document.documentElement.style.setProperty(`--color-success-light`, successLight);
document.documentElement.style.setProperty(`--color-success-dark`, successDark);
document.documentElement.style.setProperty(`--color-warning`, warning);
document.documentElement.style.setProperty(`--color-warning-light`, warningLight);
document.documentElement.style.setProperty(`--color-warning-dark`, warningDark);
document.documentElement.style.setProperty(`--color-info`, info);
document.documentElement.style.setProperty(`--color-info-light`, infoLight);
document.documentElement.style.setProperty(`--color-info-dark`, infoDark);
}
return (
<div className="w-5/6 h-1/2 text-white lg:flex justify-center items-center animate-in overflow-y-hidden">
<div className=" w-5/6 md:w-4/6 lg:w-3/6 xl:w-2/6 rounded-md bg-primary opacity-90 p-12 m-1 mt-24 shadow-lg backdrop-blur">
<div className="w-full relative">
<span className="text-2xl pb-4">Color Settings</span>
<button onClick={()=>{router.push("/admin")}} className={`float-right bg-error hover:bg-error-light ml-2 rounded p-2`}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor"
className="md:hidden size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
</svg>
<span className="hidden md:block">Back</span>
</button>
<button onClick={save} disabled={!changed}
className={`float-right ${changed ? "bg-success hover:bg-success-light" : "bg-success-dark"} rounded p-2`}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor"
className="md:hidden size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
<span className="hidden md:block">{saving?"Saving...":"Save"}</span>
</button>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Primary Color</label>
<div className="flex">
<input
value={primary}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setPrimary(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={primary}
onChange={(e) => { setPrimary(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Primary Light Color</label>
<div className="flex">
<input
value={primaryLight}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setPrimaryLight(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={primaryLight}
onChange={(e) => { setPrimaryLight(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Primary Dark Color</label>
<div className="flex">
<input
value={primaryDark}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setPrimaryDark(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={primaryDark}
onChange={(e) => { setPrimaryDark(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Secondary Color</label>
<div className="flex">
<input
value={secondary}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setSecondary(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={secondary}
onChange={(e) => { setSecondary(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Secondary Dark Color</label>
<div className="flex">
<input
value={secondaryDark}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setSecondaryDark(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={secondaryDark}
onChange={(e) => { setSecondaryDark(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Secondary Light Color</label>
<div className="flex">
<input
value={secondaryLight}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setSecondaryLight(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={secondaryLight}
onChange={(e) => { setSecondaryLight(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Error Color</label>
<div className="flex">
<input
value={error}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setError(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={error}
onChange={(e) => { setError(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Error Dark Color</label>
<div className="flex">
<input
value={errorDark}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setErrorDark(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={errorDark}
onChange={(e) => { setErrorDark(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Error Light Color</label>
<div className="flex">
<input
value={errorLight}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setErrorLight(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={errorLight}
onChange={(e) => { setErrorLight(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Success Color</label>
<div className="flex">
<input
value={success}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setSuccess(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={success}
onChange={(e) => { setSuccess(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Success Dark Color</label>
<div className="flex">
<input
value={successDark}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setSuccessDark(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={successDark}
onChange={(e) => { setSuccessDark(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
<div className="w-full relative">
<label htmlFor="primaryInput">Success Light Color</label>
<div className="flex">
<input
value={successLight}
required
type="text"
className="w-full rounded-md rounded-r-none bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setSuccessLight(e.target.value); colorChange(); }}
placeholder="Choose the primary color"
/>
<input
value={successLight}
onChange={(e) => { setSuccessLight(e.target.value); colorChange(); }}
required
type="color"
className="w-10 h-10 rounded-r p-1"
/>
</div>
</div>
</div>
</div>
);
}
export default PageComponent;

View File

@ -0,0 +1,124 @@
"use client";
import { createClient } from "@/utils/supabase/client";
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
function PageComponent() {
const supabase = createClient();
const router = useRouter();
const [name, setName] = useState<string>('');
const [price, setPrice] = useState<number>(0);
const [description, setDescription] = useState<string>('');
const [color, setColor] = useState<string>('#000000');
const getData = async () => {
}
useEffect(() => {
getData();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const data = {
name,
price,
color,
description
};
const response = await fetch('/api/tiers', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
const responseData = await response.json();
window.location.href = "/admin/tiers";
} else {
console.log(response);
}
}
return (
<div className="w-full p-8 h-1/2 text-white lg:flex justify-center items-center animate-in">
<div className="w-full lg:w-1/3 rounded-md bg-primary opacity-90 p-12 m-1 mt-32 shadow-lg backdrop-blur">
<form onSubmit={handleSubmit}>
<div className="w-full flex">
<span className="text-2xl pb-4">Creating New Tier</span>
</div>
<div className="w-full flex">
<input
value={name}
required
type="text"
onChange={(e) => { setName(e.target.value) }}
className="mr-2 rounded-md bg-info-bright p-2 w-1/2 text-black shadow-lg"
placeholder="Tag Name"
/>
<div className="flex border border-gray-300 w-1/2 rounded-md">
<span className="flex items-center bg-gray-100 rounded-l-md border-r px-3 text-gray-500">
$
</span>
<input
value={price}
onChange={(e) => { setPrice(Number(e.target.value)) }}
required
placeholder="Price in USD per month"
type="number"
className="flex-1 min-w-0 bg-info-bright rounded-r-md px-3 py-2 text-black focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
</div>
<div className="w-full pt-2 justify-center flex">
<textarea
value={description}
onChange={(e) => { setDescription(e.target.value) }}
required
placeholder="Description of the tier and what the user gets from subscribing."
rows={3}
className="w-full rounded-md bg-info-bright p-2 w-1/2 text-black shadow-lg"
></textarea>
</div>
<div className="w-full pt-2 justify-center flex">
<div className="w-1/2 relative">
<input
value={color}
required
type="text"
className="w-full rounded-md bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setColor(e.target.value) }}
placeholder="Choose the tiers color"
/>
<input
value={color}
onChange={(e) => { setColor(e.target.value) }}
required
id="colorInput"
type="color"
className="absolute right-0 top-0 w-10 h-full rounded-r p-1"
/>
</div>
<button
type="submit"
className="hover:scale-95 ml-2 shadow-lg w-1/2 h-10 text-center bg-success hover:bg-success-light text-white font-bold rounded flex items-center justify-center"
>
Create
</button>
<button
type="button"
onClick={() => { router.push("/admin/tiers") }}
className="hover:scale-95 shadow-lg ml-2 w-1/2 h-10 text-center bg-error-dark hover:bg-error text-white font-bold rounded flex items-center justify-center"
>
Back
</button>
</div>
</form>
</div>
</div>
);
}
export default PageComponent;

55
app/admin/tiers/page.tsx Normal file
View File

@ -0,0 +1,55 @@
"use client";
import { createClient } from "@/utils/supabase/client";
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
function PageComponent() {
const router = useRouter();
const supabase = createClient();
const [tiers, setTiers] = useState<any[]>([]);
const getData = async () => {
try {
const response = await fetch('/api/tiers');
if (response.ok) {
const data = await response.json();
setTiers(data);
} else {
console.error('failed to fetch tiers');
}
} catch (error) {
console.error('Error fetching users:', error);
}
}
useEffect(() => {
getData();
}, []);
return (
<div className="w-2/3 p-8 h-1/2 text-white lg:flex justify-center items-center animate-in">
<div className="w-full lg:w-1/3 rounded-md bg-primary opacity-90 p-8 m-1 mt-32 shadow-lg backdrop-blur">
<div className="w-full flex justify-center">
<span className="text-2xl pb-4">Tiers Management</span>
</div>
<div className="w-full flex justify-center">
<button onClick={() => { router.push("/admin/tiers/create") }} className="bg-success hover:bg-success-light rounded p-2 w-full">New Tier</button>
<button onClick={() => { router.push("/admin") }} className="bg-error hover:bg-error-light rounded p-2 w-full ml-2">Back</button>
</div>
<div className="w-full justify-center pt-8">
{tiers.map((item, index) => (
<div className="w-full flex justify-center">
<div key={index} className="text-white w-full text-stroke mt-2">
<button onClick={() => { router.push("/admin/tiers/view?name=" + item.name) }} className="py-2 w-full rounded hover:scale-105" style={{ backgroundColor: item.color }}>{item.name} - ${item.price}/month</button>
</div>
</div>
))}
</div>
</div>
</div>
);
}
export default PageComponent;

View File

@ -0,0 +1,141 @@
"use client";
import { createClient } from "@/utils/supabase/client";
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
function PageComponent() {
const router = useRouter();
const supabase = createClient();
const [tier, setTier] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [name, setName] = useState<string>('');
const [price, setPrice] = useState<number>(0);
const [description, setDescription] = useState<string>('');
const [color, setColor] = useState<string>('#000000');
const getData = async () => {
try {
const searchParams = new URLSearchParams(window.location.search);
const name = searchParams.get('name');
const response = await fetch('/api/tiers/'+name);
if (response.ok) {
const data = await response.json();
setTier(data);
setName(data.name);
setPrice(data.price);
setDescription(data.description);
setColor(data.color);
} else {
console.error('failed to fetch tiers');
}
} catch (error) {
console.error('Error fetching users:', error);
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const data = {
newName:name,
price,
color,
description
};
const response = await fetch('/api/tiers/'+name, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
const responseData = await response.json();
window.location.href = "/admin/tiers";
} else {
console.log(response);
}
}
useEffect(() => {
getData();
}, []);
return (
<div className="w-full p-8 h-1/2 text-white lg:flex justify-center items-center animate-in">
<div className="w-full lg:w-1/3 rounded-md bg-primary opacity-90 p-12 m-1 mt-32 shadow-lg backdrop-blur">
<form onSubmit={handleSubmit}>
<div className="w-full flex">
<span className="text-2xl pb-4">Editing Existing Tier</span>
</div>
<div className="w-full flex">
<input
value={name}
required
type="text"
onChange={(e) => { setName(e.target.value) }}
className="mr-2 rounded-md bg-info-bright p-2 w-1/2 text-black shadow-lg"
placeholder="Tag Name"
/>
<div className="flex border border-gray-300 w-1/2 rounded-md">
<span className="flex items-center bg-gray-100 rounded-l-md border-r px-3 text-gray-500">
$
</span>
<input
value={price}
onChange={(e) => { setPrice(Number(e.target.value)) }}
required
placeholder="Price in USD per month"
type="number"
className="flex-1 min-w-0 bg-info-bright rounded-r-md px-3 py-2 text-black focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
</div>
<div className="w-full pt-2 justify-center flex">
<textarea
value={description}
onChange={(e) => { setDescription(e.target.value) }}
required
placeholder="Description of the tier and what the user gets from subscribing."
rows={3}
className="w-full rounded-md bg-info-bright p-2 w-1/2 text-black shadow-lg"
></textarea>
</div>
<div className="w-full pt-2 justify-center flex">
<div className="w-1/2 relative">
<input
value={color}
required
type="text"
className="w-full rounded-md bg-info-bright p-2 text-black shadow-lg"
onChange={(e) => { setColor(e.target.value) }}
placeholder="Choose the tiers color"
/>
<input
value={color}
onChange={(e) => { setColor(e.target.value) }}
required
id="colorInput"
type="color"
className="absolute right-0 top-0 w-10 h-full rounded-r p-1"
/>
</div>
<button onClick={() => { }} className="hover:scale-95 shadow-lg ml-2 w-1/2 h-10 text-center bg-warning hover:bg-warning-light text-white font-bold rounded flex items-center justify-center">
Update
</button>
<button type="button" onClick={() => { }} className="hover:scale-95 shadow-lg ml-2 w-1/2 h-10 text-center bg-error hover:bg-error-light text-white font-bold rounded flex items-center justify-center">
Delete
</button>
<button type="button" onClick={() => { router.push("/admin/tiers") }} className="hover:scale-95 shadow-lg ml-2 w-1/2 h-10 text-center bg-error-dark hover:bg-error text-white font-bold rounded flex items-center justify-center">
Back
</button>
</div>
</form>
</div>
</div>
);
}
export default PageComponent;

View File

@ -0,0 +1,26 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { createClient } from "@/utils/supabase/server";
import { NextResponse } from "next/server";
export async function PUT(request: Request) {
const body = await request.json();
console.log(body)
const supabase = createClient();
var value = body.value;
const { error } = await supabase.from('interface_configurations').update({ value }).eq('name', body.name);
console.log(error)
if (error) {
return NextResponse.error();
}
return NextResponse.json({});
}
export async function GET(request: Request) {
const url = new URL(request.url as string, 'http://localhost');
const name = url.searchParams.get('name') || '';
const supabase = createClient();
const { data, error } = await supabase.from('interface_configurations').select('*').eq('name', name).single();
return NextResponse.json(data);
}

View File

@ -0,0 +1,17 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { createClient } from "@/utils/supabase/server";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
try {
const supabase = createClient();
const { data, error } = await supabase.auth.admin.listUsers();
console.log(error)
if (error) {
throw error;
}
return NextResponse.json(data);
} catch (error) {
return NextResponse.error();
}
}

View File

@ -8,11 +8,16 @@ export async function GET(
const galleryId = params.id; const galleryId = params.id;
const supabase = createClient(); const supabase = createClient();
const { data: gallery, error: galleryError } = await supabase
.from('galleries')
.select('*')
.eq('name', galleryId)
.single();
// List all files in the galleryId path // List all files in the galleryId path
let { data: files, error } = await supabase.storage.from('galleries').list(galleryId); let { data: files, error } = await supabase.storage.from('galleries').list(params.id.toLowerCase().replace(/\s+/g, '_'));
if (files==null || error) { if (files==null || error) {
console.error('Error listing files:', error); //console.error('Error listing files:', error);
return NextResponse.error(); return NextResponse.error();
} }

View File

@ -0,0 +1,34 @@
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const galleryId = params.id;
const supabase = createClient();
const { data: gallery, error: galleryError } = await supabase
.from('galleries')
.select('*')
.eq('name', galleryId)
.single();
// List all files in the galleryId path
let { data: files, error } = await supabase.storage
.from('galleries')
.list(params.id.toLowerCase().replace(/\s+/g, '_'));
if (files == null || error) {
//console.error('Error listing files:', error);
return NextResponse.error();
}
// Extract file names from the list of files
const fileNames = files.map((file) => file.name);
// Return a JSON response with the array of file names
return new Response(JSON.stringify(fileNames), {
headers: { 'content-type': 'application/json' },
});
}

View File

@ -1,18 +1,38 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server"; import { createClient } from "@/utils/supabase/server";
import sharp from 'sharp';
async function blurImage(blob: Buffer): Promise<Buffer> {
// Convert the blob to a sharp object
const image = sharp(blob);
// Blur the image
const blurredImage = await image.blur(75).toBuffer();
return blurredImage;
}
export async function GET( export async function GET(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: { id: string } }
) { ) {
const galleryId = params.id;
const supabase = createClient();
// List all files in the galleryId path const galleryId = params.id; const supabase = createClient();
const user = await supabase.auth.getUser();
const { data: gallery, error: galleryError } = await supabase
.from('galleries')
.select('*')
.eq('name', params.id)
.single();
// List all files in the params.id path
let { data: files, error } = await supabase.storage.from('galleries').list(galleryId); let { data: files, error } = await supabase.storage.from('galleries').list(galleryId);
if (files==null || error) { if (files==null || error) {
console.error('Error listing files:', error); //console.error('Error listing files:', error);
return NextResponse.error(); return NextResponse.error();
} }
@ -23,13 +43,38 @@ export async function GET(
let { data: blobdata, error } = await supabase.storage.from('galleries').download(galleryId+"/"+file.name); let { data: blobdata, error } = await supabase.storage.from('galleries').download(galleryId+"/"+file.name);
if (error || blobdata==null) { if (error || blobdata==null) {
console.error('Error downloading file:', error); //console.error('Error downloading file:', error);
continue; continue;
} }
let blobBuffer = Buffer.from(await blobdata.arrayBuffer());
const base64 = Buffer.from(await blobdata.arrayBuffer()).toString('base64'); let userId = user.data.user?.id;
let { data: subscription, error: rolesError } = await supabase
.from('user_subscriptions')
.select('*')
.eq('user_id', userId)
.single();
switch(gallery.tier){
case "Tier 3":
if(subscription?.tier!="Tier 3"){
blobBuffer = await blurImage(blobBuffer);
}
break;
case "Tier 2":
if(subscription?.tier!="Tier 3" && subscription?.tier!="Tier 2"){
blobBuffer = await blurImage(blobBuffer);
}
break;
case "Tier 1":
if(subscription?.tier!="Tier 3" && subscription?.tier!="Tier 2" && subscription?.tier!="Tier 1"){
blobBuffer = await blurImage(blobBuffer);
}
break;
default:
break;
}
const contentType = file.name.endsWith('.png') ? 'image/png' : 'image/jpeg'; const contentType = file.name.endsWith('.png') ? 'image/png' : 'image/jpeg';
const dataUrl = `data:${contentType};base64,${base64}`; const dataUrl = `data:${contentType};base64,${blobBuffer.toString('base64')}`;
urls.push(dataUrl); urls.push(dataUrl);
} }

View File

@ -1,29 +1,64 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server"; import { createClient } from "@/utils/supabase/server";
import sharp from 'sharp';
async function blurImage(blob: Buffer): Promise<Buffer> {
// Convert the blob to a sharp object
const image = sharp(blob);
// Blur the image
const blurredImage = await image.blur(75).toBuffer();
return blurredImage;
}
export async function GET( export async function GET(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: { id: string } }
) { ) {
const galleryId= params.id // 312
const supabase = createClient(); const supabase = createClient();
const user = await supabase.auth.getUser();
// Extract galleryId from the route value const url = new URL(request.url);
var blob = null; const search = url.searchParams.get("nsfw");
var contentType = "image/jpeg" const nsfw = search === "true";
let { data: blobdata, error } = await supabase.storage.from('galleries').download(galleryId+'/1.jpeg')
blob = blobdata; const { data: gallery, error: galleryError } = await supabase
console.log(error) .from('galleries')
if (error) { .select('*')
contentType = "image/png" .eq('name', params.id)
let { data: blobdata, error } = await supabase.storage.from('galleries').download(galleryId+'/1.png') .single();
console.log(error)
blob = blobdata; var thumbnailFile = gallery.thumbnail_file;
if(thumbnailFile==null || thumbnailFile==""){
let { data: files, error } = await supabase.storage.from('galleries').list(params.id);
if (files == null || files?.length == 0) {
return NextResponse.error();
}
thumbnailFile = files[0].name;
} }
if(blob != null){
const base64 = Buffer.from(await blob.arrayBuffer()).toString('base64'); // Loop through each file, download it, convert it to base64, and add the data URL to the array
const dataUrl = `data:${contentType};base64,${base64}`; let { data: blobdata, error: fileError } = await supabase.storage.from('galleries').download(params.id + "/" + thumbnailFile);
return new Response(dataUrl); if (fileError || blobdata == null) {
//console.error('Error downloading file:', error);
return NextResponse.error();
} }
return NextResponse.error(); let blobBuffer = Buffer.from(await blobdata.arrayBuffer());
let userId = user.data.user?.id;
let { data: subscription, error: rolesError } = await supabase
.from('user_subscriptions')
.select('*')
.eq('user_id', userId)
.single();
if(nsfw && gallery.nsfw){
blobBuffer = await blurImage(blobBuffer);
}
const contentType = thumbnailFile.endsWith('.png') ? 'image/png' : 'image/jpeg';
const dataUrl = `data:${contentType};base64,${blobBuffer.toString('base64')}`;
// Return a JSON response with the array of URLs
return new Response(dataUrl);
} }

View File

@ -0,0 +1,110 @@
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";
import path from 'path';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const id = params.id;
const supabase = createClient();
const { data: gallery, error } = await supabase.from('galleries').select("*").eq('name', id).single();
return NextResponse.json({ gallery });
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }){
const supabase = createClient();
const formData = await request.formData();
const tags = JSON.parse(formData.getAll('tags').toString()) as string[];
const originalName = formData.get('originalName');
const name = formData.get('name')?.toString();
const nsfw = formData.get('nsfw')?.toString();
const tier = formData.get('tier')?.toString();
const thumbnail = formData.get('thumbnail');
const { error } = await supabase.from('galleries').update({name, tags, nsfw, tier, thumbnail_file:thumbnail}).eq('name', originalName ?? '');
async function renameFolder(oldFolderName: any, newFolderName: string) {
// Get a list of all files in the old folder
let { data: oldFiles, error } = await supabase.storage.from('galleries').list(oldFolderName);
if (error) {
console.error('Error fetching files:', error);
return;
}
// Move each file to the new folder
if (oldFiles) {
for (let file of oldFiles) {
let oldPath = file.name;
let newPath = newFolderName + '/' + oldPath.split('/').pop();
let { error: moveError } = await supabase.storage.from('galleries').move(oldPath, newPath);
if (moveError) {
console.error(`Error moving file ${oldPath} to ${newPath}:`, moveError);
}
}
}
// Delete the old folder
let { error: deleteError } = await supabase.storage.from('galleries').remove([oldFolderName]);
if (deleteError) {
console.error('Error deleting old folder:', deleteError);
}
}
renameFolder(originalName, name ?? '');
if(error){
return NextResponse.error();
}
let { data: galleries, error:galleriesError } = await supabase
.from('galleries')
.select('*');
return NextResponse.json({ success: true, galleries });
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
const id =params.id;
const supabase = createClient();
const { data: gallery, error } = await supabase.from('galleries').delete().eq('name', id).single();
let { data: galleries, error:galleriesError } = await supabase
.from('galleries')
.select('*');
return NextResponse.json({ success: true, galleries });
}
// const tagsResponse = await fetch(`/api/galleries/tags?search=${search}`);
// const tagsData = await tagsResponse.json();
// const galleriesWithTagData = galleriesData.map((gallery: any) => {
// const tags = tagsData.filter((tag: any) => gallery.tags.includes(tag.name));
// return { ...gallery, tags };
// });
// const formData = new FormData();
// formData.append('name', name);
// formData.append('tags', JSON.stringify(tags));
// files.forEach((file: File) => {
// formData.append('files', file);
// });
// const response = await fetch('/api/galleries', {
// method: 'POST',
// body: formData,
// });
// if (response.ok) {
// const data = await response.json();
// // Handle success
// } else {
// // Handle error
// }

View File

@ -0,0 +1,37 @@
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";
import path from 'path';
export async function GET(request: Request) {
const supabase = createClient();
let { data: galleries, error } = await supabase
.from('galleries')
.select('*');
return NextResponse.json(galleries);
}
export async function POST(request: Request) {
const supabase = createClient();
const formData = await request.formData();
const files = formData.getAll('files');
const tags = JSON.parse(formData.getAll('tags').toString()) as string[];
const name = formData.get('name');
const nsfw = formData.get('nsfw');
const tier = formData.get('tier');
const thumbnail = formData.get('thumbnail');
for (let i = 0; i < files.length; i++) {
const file = files[i] as File; // Cast 'file' to 'File' type
supabase.storage.from('galleries').upload(`${name}/${file.name}`, file);
}
const { data: gallery, error } = await supabase.from('galleries').insert({ name, tags, nsfw, thumbnail_file:thumbnail, tier, column_number: 3 }).single();
let { data: galleries, error: galleriesError } = await supabase
.from('galleries')
.select('*');
return NextResponse.json({ success: true, galleries });
}

View File

@ -7,27 +7,22 @@ export async function POST(request: Request) {
const url = new URL(request.url); const url = new URL(request.url);
const search = url.searchParams.get("search"); const search = url.searchParams.get("search");
const data = await request.json(); const data = await request.json();
const tags = data.tags; const tags = data.tags as string[];
if(tags.length === 0){ if(tags.length === 0){
let { data: galleries, error } = await supabase let { data: galleries, error } = await supabase
.from('galleries') .from('galleries')
.select('*') .select('*')
.ilike('name', `%${search}%`) .ilike('name', `%${search}%`)
.ilike('description', `%${search}%`);
return NextResponse.json(galleries); return NextResponse.json(galleries);
} }
else{ else{
// Rest of the code... // Rest of the code...
let { data: galleries, error } = await supabase let { data: galleries, error } = await supabase
.from('galleries') .from('galleries')
.select('*') .select('*')
.contains('tags', tags) .contains('tags', tags) // Fix: Use contains instead of overlaps
.ilike('name', `%${search}%`) .ilike('name', `%${search}%`)
.ilike('description', `%${search}%`)
.order('created_at', { ascending: false });
return NextResponse.json(galleries);
return NextResponse.json(galleries); return NextResponse.json(galleries);
} }
} }
@ -35,6 +30,6 @@ export async function POST(request: Request) {
// const tagsResponse = await fetch(`/api/galleries/tags?search=${search}`); // const tagsResponse = await fetch(`/api/galleries/tags?search=${search}`);
// const tagsData = await tagsResponse.json(); // const tagsData = await tagsResponse.json();
// const galleriesWithTagData = galleriesData.map((gallery: any) => { // const galleriesWithTagData = galleriesData.map((gallery: any) => {
// const tags = tagsData.filter((tag: any) => gallery.tags.includes(tag.id)); // const tags = tagsData.filter((tag: any) => gallery.tags.includes(tag.name));
// return { ...gallery, tags }; // return { ...gallery, tags };
// }); // });

View File

@ -5,8 +5,28 @@ import { createClient } from "@/utils/supabase/server";
export async function GET(request: Request) { export async function GET(request: Request) {
const supabase = createClient(); const supabase = createClient();
let { data: tags, error } = await supabase let { data: tags, error } = await supabase
.from('tags') .from('tags')
.select('*') .select('*')
.order('name', { ascending: true });
return NextResponse.json(tags) return NextResponse.json(tags)
} }
export async function POST(request: Request) {
const supabase = createClient();
const data = await request.json();
const { data: tag, error } = await supabase.from('tags').insert({ name: data.tag }).single();
if (error) {
return NextResponse.error();
}
return NextResponse.json(tag);
}
export async function PUT(request: Request) {
const supabase = createClient();
const data = await request.json();
const { data: tag, error } = await supabase.from('tags').delete().eq('name', data.tag).single();
if (error) {
return NextResponse.error();
}
return NextResponse.json(tag);
}

View File

@ -0,0 +1,36 @@
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";
export async function GET(
request: Request,
{ params }: { params: { name: string } }
) {
const supabase = createClient();
const { data: tier, error: galleryError } = await supabase
.from('tiers')
.select('*')
.eq('name', params.name)
.single();
if(galleryError) {
return NextResponse.error();
}
return NextResponse.json(tier);
}
export async function PUT(
request: Request,
{ params }: { params: { name: string } }
) {
const supabase = createClient();
const { newName, price, color, description } = await request.json();
console.log(newName)
const { error } = await supabase.from('tiers')
.update({ name:newName, price, color, description }).eq('name', params.name);
if (error) {
console.error('Error updating tier:', error);
return NextResponse.error();
}
return NextResponse.json({});
}

26
app/api/tiers/route.ts Normal file
View File

@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";
export async function GET(request: Request) {
const supabase = createClient();
const { data, error } = await supabase.from('tiers').select('*');
if (error) {
console.error('Error fetching tiers:', error);
return NextResponse.error();
}
const tiers = data ?? [];
return NextResponse.json(tiers);
}
export async function POST(request: Request) {
const supabase = createClient();
const { name, price, color, description } = await request.json();
const { data, error } = await supabase.from('tiers').insert([{ name, price, color, description }]);
if (error) {
console.error('Error inserting tier:', error);
return NextResponse.error();
}
return NextResponse.json(data);
}

View File

@ -0,0 +1,168 @@
"use client";
import { createClient } from "@/utils/supabase/client";
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Masonry from "react-masonry-css";
import SearchInput from "@/components/neroshitron/search_input";
function PageComponent() {
const router = useRouter();
const [selectedGallery, setSelectedGallery] = useState<string | null>(null);
const [filePreviews, setFilePreviews] = useState<string[]>([]);
const [name, setName] = useState<string>('');
const [nsfw, setNsfw] = useState<boolean>(false);
const [tags, setTags] = useState<string[]>([]);
const [tier, setTier] = useState<string>('Free');
const [thumbnail, setThumbnail] = useState<string>("");
const [files, setFiles] = useState<FileList>();
const [tiers, setTiers] = useState<any[]>([]);
const supabase = createClient();
const user = supabase.auth.getUser();
const getData = async () => {
try {
const response = await fetch('/api/tiers');
if (response.ok) {
const data = await response.json();
setTiers(data);
} else {
console.error('failed to fetch tiers');
}
} catch (error) {
console.error('Error fetching users:', error);
}
}
useEffect(() => {
getData();
}, [selectedGallery]);
const closeGallery = () => {
setSelectedGallery(null);
}
const createGallery = async () => {
const formData = new FormData();
formData.append('name', name);
if (files) {
Array.from(files).forEach((file: File) => {
formData.append('files', file);
});
}
formData.append('tags', JSON.stringify(tags));
formData.append('nsfw', nsfw.toString());
formData.append('thumbnail', thumbnail);
formData.append('tier', tier);
const response = await fetch('/api/galleries/admin', {
method: 'POST',
body: formData,
});
if (response.ok) {
const data = await response.json();
window.location.href = "/gallery/admin/view?id="+name;
} else {
console.log(response)
}
}
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files) {
const previews: string[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const reader = new FileReader();
reader.onload = (e) => {
if (e.target && e.target.result) {
previews.push(e.target.result.toString());
if (previews.length === files.length) {
setFiles(files);
setFilePreviews(previews);
}
}
};
reader.readAsDataURL(file);
}
}
};
return (
<div className="w-full p-8 text-white flex justify-center items-center animate-in">
<div className="w-full lg:w-1/2 rounded-md bg-primary opacity-90 backdrop-blur-lg p-12 mt-32 shadow-lg">
<div className="w-full flex pb-4">
<span className="text-2xl">Creating New Gallery</span>
</div>
<div className="w-full flex">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="mb-8 mr-2 rounded-md bg-secondary p-2 w-1/2 text-white shadow-lg"
placeholder="Gallery Name"
/>
<div className="w-1/4">
<button onClick={() => router.push("/gallery/admin")} className="w-full bg-error hover:bg-error-light text-white rounded-md p-2 shadow-lg">
Back
</button>
</div>
<div className="w-1/4">
<button onClick={()=>{createGallery()}} className="w-full bg-success hover:bg-success-light text-white rounded-md p-2 ml-2 shadow-lg ">
<span></span>Create
</button>
</div>
</div>
<div className="w-full lg:flex">
<div className="w-full lg:w-1/2">
<div className="w-1/2 absolute pr-14">
<SearchInput
placeholderTags={[
{ value: "tags", label: "❗️ click here to add tags" },
]}
startingTags={tags}
nsfwButtonEnabled={true}
searchChanged={(search) => { }}
nsfwChanged={(nsfw) => { }}
tagsChanged={(tags) => { setTags(tags) }}
/>
</div>
</div>
<div className="w-full lg:w-1/2 lg:pt-0 pt-4">
<select value={nsfw ? "NSFW" : "SFW"} className="mb-2 shadow-lg rounded-md bg-secondary p-2 w-full text-white" onChange={e=>{
setNsfw(e.target.value === "NSFW");
}}>
<option value="NSFW" selected={nsfw}>NSFW</option>
<option value="SFW" selected={nsfw}>SFW</option>
</select>
<select onChange={e=>{
setTier(e.target.value);
}} className="mb-2 shadow-lg mr-2 rounded-md bg-secondary p-2 w-full text-white">
<option value="" disabled >Select New Tier</option>
{tiers.map((tier, index) => (
<option key={index} value={tier.name}>{tier.name}</option>
))}
</select>
<select onChange={e=>{setThumbnail(e.target.value)}} className="mb-2 shadow-lg mr-2 rounded-md bg-secondary p-2 w-full text-white">
<option value="" disabled selected>Select Thumbnail</option>
{files && Array.from(files).map((file: File, index: number) => (
<option key={index} value={file.name}>{file.name}</option>
))}
</select>
<input
className="relative m-0 block w-full min-w-0 flex-auto cursor-pointer rounded border border-solid border-secondary-lighter bg-transparent bg-clip-padding px-3 py-[0.32rem] text-base font-normal text-surface transition duration-300 ease-in-out file:-mx-3 file:-my-[0.32rem] file:me-3 file:cursor-pointer file:overflow-hidden file:rounded-none file:border-0 file:border-e file:border-solid file:border-inherit file:bg-transparent file:px-3 file:py-[0.32rem] file:text-surface focus:border-primary focus:text-gray-700 focus:shadow-inset focus:outline-none dark:border-white/70 dark:text-white file:dark:text-white"
type="file"
id="formFileMultiple"
multiple
onChange={handleFileChange}
/>
<Masonry breakpointCols={3} className="my-masonry-grid pl-6 col-span-2">
{filePreviews.map((preview, index) => (
<img key={index} src={preview} alt={`Preview ${index}`} />
))}
</Masonry>
</div>
</div>
</div>
</div>
);
}
export default PageComponent;

161
app/gallery/admin/page.tsx Normal file
View File

@ -0,0 +1,161 @@
"use client";
import { createClient } from "@/utils/supabase/client";
import React, { useState, useEffect } from 'react';
import SearchInput from "@/components/neroshitron/search_input";
import { useRouter } from 'next/navigation';
function PageComponent() {
const router = useRouter();
const supabase = createClient();
const user = supabase.auth.getUser();
const [tags, setTags] = useState<any[]>([]);
const [nsfwState, setNsfwState] = useState<boolean>(false);
const [tagsState, setTagsState] = useState<string[]>([]);
const [searchState, setSearchState] = useState<string>("");
const [galleries, setGalleries] = useState([]);
const [tagSearch, setTagSearch] = useState<string>('');
const [newTagName, setNewTagName] = useState<string>('');
const getData = async () => {
const tagsResponse = await fetch(`/api/galleries/tags`);
const tagsData = await tagsResponse.json();
setTags(tagsData);
const galleriesResponse = await fetch(`/api/galleries?search=` + searchState + '&nsfw=' + nsfwState, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tags: tagsState })
});
const galleriesData = await galleriesResponse.json();
setGalleries(galleriesData);
}
const createTag = async () => {
let formattedTag = newTagName.toLowerCase().replace(" ", "_");
const tagsResponse = await fetch(`/api/galleries/tags`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tag: formattedTag })
});
const tagsData = await tagsResponse.json();
setNewTagName('');
getData();
}
const deleteTag = async (tagParam: string) => {
const tagsResponse = await fetch(`/api/galleries/tags`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tag:tagParam })
});
const tagsData = await tagsResponse.json();
getData();
}
useEffect(() => {
getData();
}, [tagsState, newTagName]);
return (
<div className="w-full p-8 h-max text-white lg:flex justify-center items-start animate-in mt-32">
<div className="w-full h-max lg:w-1/3 rounded-md bg-primary opacity-90 p-8 m-1 shadow-lg backdrop-blur">
<span className="text-2xl">Tags Management</span>
<div className="w-full flex pt-4">
<form onSubmit={createTag} className="flex w-full">
<input value={newTagName} required type="text" onChange={(e)=>{setNewTagName(e.target.value)}} className=" mb-8 mr-2 rounded-md bg-info-bright p-2 w-1/2 text-black shadow-lg" placeholder="Tag Name" />
<button className=" ml-2 shadow-lg w-1/2 h-10 text-center bg-success hover:bg-success-light text-white font-bold rounded flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="lg:hidden size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span className="lg:hidden block">Tag</span>
<span className="lg:block hidden">New Tag</span>
</button>
</form>
</div>
<div className="w-full flex">
<input type="text" value={tagSearch} onChange={(e)=>{setTagSearch(e.target.value)}} className=" mb-8 shadow-lg mr-2 rounded-md bg-info-bright p-2 w-full text-black" placeholder="Search all tags by name" />
</div>
<div className="w-full h-96 overflow-y-scroll no-scrollbar">
<table className="w-full bg-primary-light rounded">
<tbody>
{tags.filter((value,index,array)=>{
return value.name.toLowerCase().includes(tagSearch.toLowerCase());
}).map((item:any) => (
<tr key={item.name} className="hover:bg-secondary-dark animate-in shadow">
<td className="px-4 py-2">{item.name}</td>
<td className="px-4 py-2">
<button onClick={()=>{deleteTag(item.name)}} className=" bg-error shadow-lg hover:bg-error-light text-white font-bold py-2 px-4 rounded float-right">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="w-full h-max lg:w-1/2 rounded-md bg-primary opacity-90 backdrop-blur-lg p-8 m-1 shadow-lg">
<div className="w-full pb-2 flex justify-between">
<span className="text-2xl">Galleries Management</span>
<div>
<button onClick={()=>{router.push("/admin/")}} className="ml-2 p-2 shadow-lg h-10 text-center bg-error hover:bg-error-light text-white font-bold rounded">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="xl:hidden size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span className="xl:block hidden">Back</span>
</button>
<button onClick={()=>{router.push("/gallery/admin/create")}} className="ml-2 p-2 shadow-lg h-10 text-center bg-success hover:bg-success-light text-white font-bold rounded">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="xl:hidden size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span className="xl:block hidden">New Gallery</span>
</button>
</div>
</div>
<div className="w-full pb-2 flex">
<div className="absolute w-full z-20 pr-16">
<SearchInput
startingTags={[]}
placeholderTags={[
{ value: "tags", label: "❗️ click here to add tags to search" }
]} nsfwButtonEnabled={false} searchChanged={(search) => { setSearchState(search) }} nsfwChanged={(nsfw) => { setNsfwState(nsfw) }} tagsChanged={(tags) => { setTagsState(tags) }} />
</div>
</div>
<div className="w-full h-96 overflow-y-scroll no-scrollbar">
<table className="w-full mt-20 bg-primary-light rounded">
<tbody>
{/* Replace this with your data mapping logic */}
{galleries.map((item: { name: string, imageCount: number, tier: string }) => (
<tr key={item.name} className="hover:bg-secondary-dark shadow animate-in">
<td className="px-4 py-2" style={{ width: '65%' }}>{item.name}</td>
<td className="px-4 py-2">{item.imageCount}</td>
<td className="px-4 py-2">{item.tier}</td>
<td className="px-4 py-2">
<button onClick={()=>{ router.push(`/gallery/admin/view?id=${encodeURIComponent(item.name)}`)}} className="bg-secondary shadow-lg hover:bg-secondary-light text-white font-bold py-2 px-4 rounded float-right">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Zm3.75 11.625a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
export default PageComponent;

View File

@ -0,0 +1,271 @@
"use client";
import { createClient } from "@/utils/supabase/client";
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Gallery from "@/components/neroshitron/gallery";
import SearchInput from "@/components/neroshitron/search_input";
import GalleryThumbnail from "@/components/neroshitron/gallery_thumbnail";
function PageComponent() {
const [filePreviews, setFilePreviews] = useState<string[]>([]);
const supabase = createClient();
const user = supabase.auth.getUser();
const [gallery , setGallery] = useState<any>(null);
const [originalName, setOriginalGalleryName] = useState<string>('');
const [galleryName, setGalleryName] = useState<string>('');
const [nsfw, setNsfw] = useState<boolean>(false);
const [tags, setTags] = useState<string[]>([]);
const [tier, setTier] = useState<string>('Free');
const [thumbnail, setThumbnail] = useState<string>();
const [fileNames, setFileNames] = useState<string[]>([]);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const router = useRouter();
const [tiers, setTiers] = useState<any[]>([]);
const [open, setOpen] = useState<boolean>(false);
const [images, setImages] = useState<string[]>([]);
const getData = async () => {
try {
const response = await fetch('/api/tiers');
if (response.ok) {
const data = await response.json();
setTiers(data);
} else {
console.error('failed to fetch tiers');
}
} catch (error) {
console.error('Error fetching users:', error);
}
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');
const galleryResponse = await fetch(`/api/galleries/admin/${id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const galleryData = await galleryResponse.json();
setGallery(galleryData.gallery);
const filesResponse = await fetch(`/api/galleries/${id}/images/names`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const filesData = await filesResponse.json();
setFileNames(filesData);
setNsfw(galleryData.gallery.nsfw);
setTags(galleryData.gallery.tags);
setTier(galleryData.gallery.tier);
setGalleryName(galleryData.gallery.name);
if(originalName === ''){
setOriginalGalleryName(galleryData.gallery.name);
}
const imagesResponse = await fetch('/api/galleries/' + galleryData.gallery.name+ '/images');
const imagesUrls = await imagesResponse.json() as string[];
setImages(imagesUrls);
}
useEffect(() => {
getData();
}, []);
useEffect(() => {
}, [gallery]);
useEffect(() => {
}, [gallery, ]);
useEffect(() => {
}, [ nsfw ]);
useEffect(() => {
}, [tags ]);
useEffect(() => {
}, [galleryName]);
useEffect(() => {
}, [ tier]);
const updateGallery = async () => {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');
const formData = new FormData();
formData.append('id', gallery.name);
formData.append('name', galleryName);
formData.append('thumbnail', thumbnail ?? '');
formData.append('originalName', originalName);
formData.append('tags', JSON.stringify(selectedTags));
formData.append('nsfw', nsfw.toString());
formData.append('tier', tier);
const response = await fetch(`/api/galleries/admin/${originalName}`, {
method: 'PUT',
body: formData
});
if (response.ok) {
console.log(response)
const data = await response.json();
} else {
console.log(response)
}
if(originalName != galleryName){
router.push(`/gallery/admin/view?id=${galleryName}`)
}
else{
window.location.reload();
}
}
const deleteGallery = async () => {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');
const response = await fetch(`/api/gallery/admin/${gallery.name}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
router.push("/gallery/admin");
} else {
console.log(response)
}
}
const tierObj = tiers.find((tier) => tier.name == gallery.tier);
const subscriptionColor = tier ? tierObj.color : null;
return (
<div className="w-full p-8 h-screen text-white flex justify-center items-center animate-in">
<div className="w-full lg:w-1/2 rounded-md p-12 mt-14 ">
<div className="w-full lg:pt-0 pt-32 flex pb-60 justify-center"> {/* Center the gallery thumbnail */}
{gallery != null && (
<GalleryThumbnail
key={"galleryThumbnail"+galleryName+"-"+tags.join("")}
id={originalName}
columns={3}
onSelect={function (id: string, columns: number): void { setOpen(true) }}
title={galleryName}
subscription={tier}
tags={tags}
subscriptionColor={subscriptionColor}
showNsfw={false}
nsfw={nsfw}
></GalleryThumbnail>
)}
</div>
<div className="w-full opacity-90 backdrop-blur-lg bg-primary shadow-lg p-8 pb-0 rounded">
<span className="text-2xl">Editing Gallery</span>
<div className="w-full flex justify-end">
<div className="w-1/2 flex">
<input
type="text"
className="mb-8 mr-2 rounded-md bg-secondary p-2 w-full text-white"
placeholder="Gallery Name"
value={galleryName}
onChange={(e) => setGalleryName(e.target.value)}
/>
</div>
<div className="w-1/2 flex">
<button
onClick={() => deleteGallery()}
className="h-10 text-center w-full bg-error hover:bg-error-light text-white rounded-md p-2 flex items-center justify-center"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5}
stroke="currentColor" className="md:hidden size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
<span className="md:block hidden">Delete</span>
</button>
<button
onClick={() => (router.push("/gallery/admin"))}
className="h-10 w-full bg-error-dark hover:bg-error text-white rounded-md p-2 ml-2 flex items-center justify-center"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5}
stroke="currentColor" className="md:hidden size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
</svg>
<span className="md:block hidden">Back</span>
</button>
<button onClick={()=>{updateGallery()}} className="h-10 w-full bg-warning hover:bg-warning-light text-white rounded-md p-2 ml-2">
<span>Update</span>
</button>
</div>
</div>
</div>
<div className="w-full lg:flex opacity-90 backdrop-blur-lg bg-primary shadow-lg p-8 pt-0 rounded">
<div className="w-full lg:w-1/2 mr-44">
<div className="w-1/2 fixed mr-8">
{gallery &&(
<SearchInput
placeholderTags={[
{ value: "tags", label: "❗️ click here to add tags" },
]}
startingTags={gallery.tags}
nsfwButtonEnabled={true}
searchChanged={(search) => {}}
nsfwChanged={(nsfw) => {}}
tagsChanged={(tags) => { setSelectedTags(tags) }}
/>
)}
</div>
</div>
<div className="w-full lg:w-1/2 pt-4">
{gallery != null && (<>
<select value={nsfw ? "NSFW" : "SFW"} className="mb-2 shadow-lg rounded-md bg-secondary p-2 w-full text-white" onChange={e=>{
setNsfw(e.target.value == "NSFW");
}}>
<option value="" disabled >Set NSFW</option>
<option value="NSFW" selected={nsfw}>NSFW</option>
<option value="SFW" selected={!nsfw}>SFW</option>
</select>
<select onChange={e=>{
setTier(e.target.value);
}} className="mb-2 shadow-lg mr-2 rounded-md bg-secondary p-2 w-full text-white">
<option value="" disabled >Select New Tier</option>
{tiers.map((tier, index) => (
<option selected={tier.name==gallery.tier} key={index} value={tier.name}>{tier.name}</option>
))}
</select>
<select onChange={e=>{setThumbnail(e.target.value)}} className="mb-2 shadow-lg mr-2 rounded-md bg-secondary p-2 w-full text-white">
<option value="" disabled selected>Select New Thumbnail</option>
{fileNames.map((name, index) => (
<option selected={name==gallery.thumbnail_file} key={index} value={name}>{name}</option>
))}
</select>
</>
)}
</div>
</div>
{(open) && (
<>
{/*
This is the modal for holding the gallery
*/}
<div
className={`fixed inset-0 transition-opacity z-30 animate-in`}
aria-hidden="true"
>
<div
className="absolute w-full h-full inset-0 bg-secondary-dark opacity-70 z-30"
onClick={() => setOpen(true)}
></div>
<div className="absolute inset-0 overflow-y-auto overflow-x-hidden no-scrollbar pt-2 w-full p-20 h-full z-30">
<Gallery
id={gallery.name}
columns={3}
closeMenu={() => setOpen(false)}
></Gallery>
</div>
</div>
</>
)}
</div>
</div>
);
}
export default PageComponent;

View File

@ -1,166 +1,57 @@
"use client"; "use client";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
import { redirect } from "next/navigation";
import GalleryThumbnail from "@/components/ui/gallery_thumbnail";
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { User } from "@supabase/supabase-js"; import Search from "@/components/neroshitron/search";
import Gallery from "@/components/ui/gallery"; import Gallery from "@/components/neroshitron/gallery";
import Link from "next/link";
function PageComponent() { function PageComponent() {
const [selectedGallery, setSelectedGallery] = useState<string | null>(null);
const supabase = createClient(); const supabase = createClient();
const [randomIds, setRandomIds] = useState<string[]>([]); // replace any with your gallery type
const [isOpen, setIsOpen] = useState<boolean>(false);
const [galleries, setGalleries] = useState<any[]>([]); // replace any with your gallery type
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [selectedGallery, setSelectedGallery] = useState<string | null>(null);
const [tags, setTags] = useState<any[]>([]);
const [search, setSearch] = useState<string>('');
const [galleryColumns, setColumns] = useState<number>(0);
const [selectedTags, setSelectedTags] = useState<number[]>([]);
const generateRandomString = function (length:number) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for ( let i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const selectGallery = (gallery:string, columns:number) => {
setRandomIds([generateRandomString(3), generateRandomString(3), generateRandomString(3), generateRandomString(3)]);
setSelectedGallery(gallery);
setColumns(columns);
setIsOpen(true);
};
const closeGallery = () => {
setSelectedGallery(null);
setColumns(0);
setIsOpen(false);
}
const getData = async () => { const getData = async () => {
let { data: { user } } = await supabase.auth.getUser();
const galleriesResponse = await fetch(`/api/galleries?search=`+search, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tags:selectedTags })
});
console.log(galleriesResponse)
const galleriesData = await galleriesResponse.json();
const tagsResponse = await fetch(`/api/galleries/tags`);
const tagsData = await tagsResponse.json();
setGalleries(galleriesData);
setTags(tagsData);
setUser(user);
setLoading(false);
} }
useEffect(() => { useEffect(() => {
getData(); getData();
}, [selectedGallery]);
}, [selectedTags,search]);
const closeGallery = () => {
setSelectedGallery(null);
}
const handleTagClick = (tag: number) => {
if (selectedTags.includes(tag)) {
setSelectedTags(selectedTags.filter((selectedTag) => selectedTag !== tag));
} else {
setSelectedTags([...selectedTags, tag]);
}
console.log(selectedTags)
};
return ( return (
user ? ( <div className="w-full">
<div className="w-full h-full flex justify-center "> <div className="w-2/4">
<div className="flex-1 w-full h-full flex flex-col gap-20"> <Search gallerySelected={(gallery:string)=>{setSelectedGallery(gallery)}}/>
<>
<div className="absolute pl-8 w-2/4 left-1/2 h-full overflow-hidden z-20 animate-in animate-duration-3000 animate-ease-out">
<section className="neroshi-blue-900 h-50 p-8 pt-20 opacity-30 hover:opacity-100">
<div className="container mx-auto py-8">
<input
className="w-full text-neroshi-blue-950 h-16 px-3 rounded mb-8 focus:outline-none focus:shadow-outline text-xl px-8 shadow-lg"
type="search"
placeholder="Search..."
onChange={(e) => setSearch(e.target.value)}
/>
<nav className="grid grid-cols-4 gap-4">
{tags.map((tag, index) => (
<a
key={index}
className={`rounded-lg no-underline text-white py-3 px-4 font-medium text-center ${
selectedTags.includes(tag.id) ? 'bg-neroshi-blue-950 hover:bg-neroshi-blue-900' : 'bg-neroshi-blue-800 hover:bg-neroshi-blue-700'
}`}
href="#"
onClick={() => handleTagClick(tag.id)}
>
{tag.name}
</a>
))}
</nav>
</div>
</section>
</div>
<div className="absolute w-full h-full overflow-hidden z-0 animate-flip-up animate-ease-out">
<img
src="gallery_girl.png"
className="float-right object-cover h-screen w-3/6"
alt="Background"
/>
</div>
<div className="absolute items-center w-2/4 h-full ml-10 z-0 overflow-hidden animate-in animate-ease-out">
<div className="grid grid-cols-3 gap-y-36 gap-x-10 h-full overflow-y-auto no-scrollbar pt-20">
{galleries && galleries.map((gallery, index) => (
<GalleryThumbnail
key={index}
id={gallery.id}
title={gallery.name}
columns={gallery.columns}
subscription={gallery.tier as string}
onSelect={selectGallery}
></GalleryThumbnail>
))}
<div className="pt-10"></div>
<div className="pt-10"></div>
</div>
</div>
</>
</div>
{isOpen ? (
<>
<div
className={`fixed inset-0 transition-opacity z-30 ${
isOpen ? "animate-in" : "fade-out"
}`}
aria-hidden="true"
>
<div
className="absolute inset-0 bg-neroshi-blue-900 opacity-70 z-30"
onClick={() => setIsOpen(false)}
></div>
<div className="absolute inset-0 overflow-y-auto overflow-x-hidden no-scrollbar pt-2 w-full p-20 z-30">
<Gallery
id={selectedGallery as string}
columns={galleryColumns}
closeMenu={() => closeGallery()}
></Gallery>
</div>
</div>
</>
) : null}
</div> </div>
) : (
<h1>loading</h1> {selectedGallery!=null ? (
) <>
{/*
This is the modal for holding the gallery
*/}
<div
className={`fixed inset-0 transition-opacity z-30 animate-in`}
aria-hidden="true"
>
<div
className="absolute inset-0 bg-secondary-dark opacity-70 z-30"
onClick={() => closeGallery()}
></div>
<div className="absolute inset-0 overflow-y-auto overflow-x-hidden no-scrollbar pt-2 w-full p-20 z-30">
<Gallery
id={selectedGallery as string}
columns={3}
closeMenu={() => closeGallery()}
></Gallery>
</div>
</div>
</>
) : null}
</div>
); );
} }

View File

@ -3,27 +3,20 @@
@tailwind utilities; @tailwind utilities;
@keyframes expandFromLeft {
0% {
width: 0;
}
100% {
width: 100%;
}
}
/* Hide scrollbar for Chrome, Safari and Opera */ /* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar { .no-scrollbar::-webkit-scrollbar {
display: none; display: none;
} }
.my-masonry-grid {
display: -webkit-box; /* Not needed if autoprefixing */
display: -ms-flexbox; /* Not needed if autoprefixing */
display: flex;
margin-left: -30px; /* gutter size offset */
width: auto;
}
.my-masonry-grid_column {
padding-left: 30px; /* gutter size */
background-clip: padding-box;
}
/* Style your items */
.my-masonry-grid_column > div { /* change div to reference your elements you put in <Masonry> */
background: grey;
margin-bottom: 30px;
}
/* Hide scrollbar for IE, Edge and Firefox */ /* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar { .no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
@ -54,10 +47,22 @@
} }
} }
@layer base { .my-masonry-grid {
* { display: -webkit-box; /* Not needed if autoprefixing */
@apply border-foreground/20; display: -ms-flexbox; /* Not needed if autoprefixing */
} display: flex;
margin-left: -30px; /* gutter size offset */
width: auto;
}
.my-masonry-grid_column {
padding-left: 30px; /* gutter size */
background-clip: padding-box;
}
/* Style your items */
.my-masonry-grid_column > div { /* change div to reference your elements you put in <Masonry> */
background: grey;
margin-bottom: 30px;
} }
.animate-in { .animate-in {
@ -74,3 +79,33 @@
transform: translateY(0); transform: translateY(0);
} }
} }
button:disabled {
filter: grayscale(50%);
}
:root {
--color-primary: #201240;
--color-primary-light: #403260;
--color-primary-dark: #100120;
--color-secondary: #4F3D70;
--color-secondary-light: #6F5D90;
--color-secondary-dark: #2F1D50;
--color-error: #862117;
--color-error-light: #C44C4C;
--color-error-dark: #5C0D0D;
--color-success: #00C9A6;
--color-success-light: #20E9C6;
--color-success-dark: #00A986;
--color-warning: #E17558;
--color-warning-light: #E39578;
--color-warning-dark: #C15538;
--color-info: #222140;
--color-info-light: #424260;
--color-info-dark: #020120;
}

View File

@ -1,7 +1,11 @@
import { GeistSans } from "geist/font/sans"; import { GeistSans } from "geist/font/sans";
import "./globals.css"; import "./globals.css";
import NavigationBar from "@/components/NavigationBar"; import { createClient } from "@/utils/supabase/client";
import NavigationBar from "@/components/neroshitron/navigation_bar";
import { SpeedInsights } from "@vercel/speed-insights/next"
import { Analytics } from "@vercel/analytics/react"
import RightHandLayoutImage from "@/components/neroshitron/right_hand_layout_image";
import Theme from "@/components/theme";
const defaultUrl = process.env.VERCEL_URL const defaultUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}` ? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000"; : "http://localhost:3000";
@ -12,6 +16,10 @@ export const metadata = {
description: "The fastest way to build apps with Next.js and Supabase", description: "The fastest way to build apps with Next.js and Supabase",
}; };
const supabase = createClient();
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
@ -20,10 +28,14 @@ export default function RootLayout({
return ( return (
<html lang="en" className={GeistSans.className}> <html lang="en" className={GeistSans.className}>
<body className="bg-background text-foreground"> <body className="bg-background text-foreground">
<div className="w-full absolute z-30"> <Theme/>
<RightHandLayoutImage/>
<div className="w-full fixed z-30 text-white white">
<NavigationBar/> <NavigationBar/>
<SpeedInsights/>
<Analytics/>
</div> </div>
<main className="min-h-screen flex flex-col items-center bg-gradient-to-r from-neroshi-blue-900 to-neroshi-blue-950"> <main className="min-h-screen flex flex-col items-center bg-gradient-to-r from-primary to-secondary overflow-hidden">
{children} {children}
</main> </main>
</body> </body>

View File

@ -25,7 +25,7 @@ export default async function Login({
const password = formData.get("password") as string; const password = formData.get("password") as string;
const supabase = createClient(); const supabase = createClient();
const { error } = await supabase.auth.signInWithPassword({ const { data:data, error } = await supabase.auth.signInWithPassword({
email, email,
password, password,
}); });
@ -64,11 +64,11 @@ export default async function Login({
<div className="flex-1 w-full flex flex-col gap-20 items-center animate-in"> <div className="flex-1 flex flex-col w-full px-8 sm:max-w-md justify-center gap-2"> <div className="flex-1 w-full flex flex-col gap-20 items-center animate-in"> <div className="flex-1 flex flex-col w-full px-8 sm:max-w-md justify-center gap-2">
<form className="animate-in flex-1 flex flex-col w-full my-32 gap-2 text-foreground"> <form className="animate-in flex-1 flex flex-col w-full my-32 gap-2 text-white">
<Link <Link
href="/" href="/"
className="absolute left-1 top-44 py-2 px-4 rounded-md no-underline text-foreground flex items-center group text-sm" className="absolute left-1 top-44 py-2 px-4 rounded-md no-underline text-white flex items-center group text-sm"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -87,36 +87,36 @@ export default async function Login({
Back Back
</Link> </Link>
<input <input
className="rounded-md px-4 py-2 bg-inherit border mb-2 mx-1" className="rounded-md px-4 py-2 bg-inherit border mb-2 mx-1 w-full sm:w-auto"
name="email" name="email"
placeholder="Email Address" placeholder="Email Address"
required required
/> />
<input <input
className="rounded-md px-4 py-2 bg-inherit border mb-2 mx-1" className="rounded-md px-4 py-2 bg-inherit border mb-2 mx-1 w-full sm:w-auto"
type="password" type="password"
name="password" name="password"
placeholder="Password " placeholder="Password "
required required
/> />
<div className="flex"> <div className="flex text-white white">
<SubmitButton <SubmitButton
formAction={signIn} formAction={signIn}
className="bg-neroshi-blue-500 hover:bg-neroshi-blue-400 rounded-md px-4 py-2 text-foreground mb-2 mx-1 w-1/2" className="bg-success hover:bg-success-light rounded-md px-4 py-2 text-white mb-2 mx-1 w-1/2"
pendingText="Signing In..." pendingText="Signing In..."
> >
Sign In Sign In
</SubmitButton> </SubmitButton>
<SubmitButton <SubmitButton
formAction={signUp} formAction={signUp}
className="bg-neroshi-blue-300 hover:bg-neroshi-blue-200 border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2 mx-1 w-1/2" className="bg-info hover:bg-info-light border border-foreground/20 rounded-md px-4 py-2 text-white mb-2 mx-1 w-1/2"
pendingText="Signing Up..." pendingText="Signing Up..."
> >
Sign Up Sign Up
</SubmitButton> </SubmitButton>
</div> </div>
{searchParams?.message && ( {searchParams?.message && (
<p className="mt-4 bg-foreground/10 mt-14 p-2 text-foreground text-center"> <p className="mt-4 bg-foreground/10 mt-14 p-2 text-white text-center">
{searchParams.message} {searchParams.message}
</p> </p>
)} )}

View File

@ -0,0 +1,12 @@
import { NextRequest, NextResponse } from 'next/server'
export function middleware(req:NextRequest, res:NextResponse) {
const requestHeaders = new Headers(req.headers)
requestHeaders.set('x-path', req.nextUrl.pathname)
return NextResponse.next({
request: {
headers: requestHeaders
}
})
}

View File

@ -1,121 +0,0 @@
import { createClient } from "@/utils/supabase/server";
import Link from "next/link";
import { redirect } from "next/navigation";
import crypto from 'crypto';
export default async function AuthButton() {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const signOut = async () => {
"use server";
const supabase = createClient();
await supabase.auth.signOut();
return redirect("/login");
};
if(user){
let email = user.email;
if(email != null){
const emailHash = crypto.createHash('md5').update(email.trim().toLowerCase()).digest('hex');
const gravatarUrl = `https://www.gravatar.com/avatar/${emailHash}`;
return(
<div className="flex justify-center items-center pt-2 ">
<nav className="w-1/3 bg-neroshi-blue-300 bg-opacity-10 flex justify-center z-10 h-16 animate-in rounded-3xl" style={{ backdropFilter: 'blur(10px)' }}>
<div className="w-full max-w-2xl flex justify-between items-center p-3 text-sm">
<div className="flex items-center gap-2 z-10">
<Link
href="/gallery"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Gallery
</Link>
<Link
href="/livestream"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Stream
</Link>
<Link
href="/commissions"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Commissions
</Link>
<Link
href="/subscriptions"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Subscription
</Link>
</div>
<div className="flex items-center gap-2">
<form action={signOut}>
<button className="py-2 px-4 rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800">
Logout
</button>
</form>
<img src={gravatarUrl} alt="Profile" className="w-10 h-10 object-cover rounded-full cursor-pointer" />
</div>
</div>
</nav>
</div>)
}
}
else{
return( <div className="flex justify-center items-center pt-2 ">
<nav className="w-1/3 bg-neroshi-blue-300 bg-opacity-10 flex justify-center h-16 animate-in rounded-3xl" style={{ backdropFilter: 'blur(10px)' }}>
<div className="w-full max-w-2xl flex justify-between items-center p-3 text-sm">
<div className="flex items-center gap-2 ">
<Link
href="/gallery"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Gallery
</Link>
<Link
href="/livestream"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Stream
</Link>
<Link
href="/commissions"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Commissions
</Link>
<Link
href="/subscriptions"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Subscription
</Link>
</div>
<div className="flex items-center gap-2">
<Link
href="/login"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Login
</Link>
<Link
href="/login"
className="py-2 px-3 flex rounded-3xl no-underline bg-neroshi-blue-900 hover:bg-neroshi-blue-800"
>
Signup
</Link>
</div>
</div>
</nav>
</div>)
}
}

View File

@ -0,0 +1,81 @@
"use client;"
import React, { useState, useEffect } from 'react';
import GalleryThumbnail from './gallery_thumbnail';
interface TagProps {
nsfw: boolean;
tags: string[];
search: string;
gallerySelected: (gallery: string) => void;
}
const Galleries = ({ nsfw, tags, search, gallerySelected }: TagProps) => {
const [galleries, setGalleries] = useState([]);
const [nsfwState, setNsfwState] = useState<boolean>(nsfw);
const [tagsState, setTagsState] = useState<string[]>(tags);
const [searchState, setSearchState] = useState<string>(search);
const [selectedGallery, setSelectedGallery] = useState<string | null>(null);
const [tiers, setTiers] = useState<any[]>([]);
const selectGallery = (gallery: string) => {
setSelectedGallery(gallery);
gallerySelected(gallery);
};
const getData = async () => {
try {
const response = await fetch('/api/tiers');
if (response.ok) {
const data = await response.json();
setTiers(data);
} else {
console.error('failed to fetch tiers');
}
} catch (error) {
console.error('Error fetching users:', error);
}
const galleriesResponse = await fetch(`/api/galleries?search=` + searchState + '&nsfw=' + nsfwState, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tags: tagsState })
});
const galleriesData = await galleriesResponse.json();
setGalleries(galleriesData);
}
useEffect(() => {
getData();
}, [tagsState]);
return (
<div className="absolute inset-0 mx-auto ml-16 md:ml-0 pt-48 p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-48 gap-x-4 animate-in overflow-y-scroll no-scrollbar z-0">
{galleries && galleries.map((gallery: any, index) => {
const tier = tiers.find((tier) => tier.name == gallery.tier);
const subscriptionColor = tier ? tier.color : null;
return (
<GalleryThumbnail
key={gallery.name + " " + nsfw}
id={gallery.name}
title={gallery.name}
tags={gallery.tags}
columns={gallery.columns}
showNsfw={nsfw}
subscription={gallery.tier as string}
subscriptionColor={subscriptionColor}
onSelect={selectGallery}
nsfw={gallery.nsfw}
></GalleryThumbnail>
);
})}
</div>
);
};
export default Galleries;

View File

@ -0,0 +1,222 @@
import { use, useState, useRef } from 'react';
import { useEffect } from 'react';
import { render } from 'react-dom';
import Masonry from 'react-masonry-css';
import PanZoom, { PanZoomRef } from 'react-easy-panzoom';
interface GalleryProps {
id: string;
columns: number;
closeMenu: () => void;
}
const Gallery = ({ id, columns, closeMenu }: GalleryProps) => {
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [images, setImages] = useState<string[]>([]);
const [galleryId, setGalleryId] = useState(id as string);
const [currentIndex, setCurrentIndex] = useState(0);
const panZoomRef = useRef<any>(null);
const next = () => {
if (currentIndex < images.length - 1) {
setCurrentIndex(currentIndex + 1);
} else {
setCurrentIndex(0);
}
}
const previous = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
} else {
setCurrentIndex(images.length - 1);
}
}
const handleDownload = (image: string) => {
const link = document.createElement('a');
link.href = image;
link.download = 'image.jpg'; // or any other filename
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const getData = async () => {
const thumbnailResponse = await fetch('/api/galleries/' + String(galleryId) + '/images');
const thumbnailUrl = await thumbnailResponse.json() as string[];
setImages(thumbnailUrl);
}
useEffect(() => {
getData();
const handleKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowLeft':
case 'a':
case 'A':
previous();
break;
case 'ArrowRight':
case 'd':
case 'D':
next();
break;
case 'Escape':
close();
break;
default:
break;
}
};
setSelectedImage(images[currentIndex]);
window.addEventListener('keydown', handleKeyDown);
// Clean up the event listener when the component is unmounted
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [selectedImage, currentIndex]);
const handleClick = (image: string) => {
setSelectedImage(image);
setCurrentIndex(images.indexOf(image));
};
const resetPanZoom = () => {
panZoomRef.current.autoCenter();
};
const close = () => {
if (selectedImage != null) {
setSelectedImage(null);
setImages([]);
}
else {
closeMenu();
}
};
const back_page = () => {
if (selectedImage != null) {
setSelectedImage(null);
setImages([]);
}
};
const renderButtons = () => {
return (
<div className="z-20 bottom-10 fixed text-white pt-4 bg-primary bg-opacity-90 animate-in rounded-md" style={{ backdropFilter: 'blur(10px)' }}>
<div className='grid grid-cols-5 pl-4 gap-4 pr-4'>
<button
className={`justify-center text-center w-full animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50 bg-error hover:bg-error-light`}
onClick={() => close()}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
<button
className={`justify-center text-center w-full animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50 ${!selectedImage ? 'opacity-50 cursor-not-allowed bg-gray-800' : 'bg-secondary hover:bg-secondary-light'}`}
onClick={() => resetPanZoom()}
disabled={!selectedImage}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9V4.5M9 9H4.5M9 9 3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5 5.25 5.25" />
</svg>
</button>
<button
className={`justify-center text-center w-full animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50 ${!selectedImage ? 'opacity-50 cursor-not-allowed bg-gray-800' : 'bg-secondary hover:bg-secondary-light'}`}
onClick={() => previous()}
disabled={!selectedImage}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5" />
</svg>
</button>
<button
className={`justify-center text-center w-full animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50 ${!selectedImage ? 'opacity-50 cursor-not-allowed bg-gray-800' : 'bg-secondary hover:bg-secondary-light'}`}
onClick={() => next()}
disabled={!selectedImage}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5" />
</svg>
</button>
<button
className={`justify-center text-center w-full animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50 ${!selectedImage ? 'opacity-50 cursor-not-allowed bg-gray-800' : 'bg-success hover:bg-success-light'}`}
onClick={() => selectedImage && handleDownload(selectedImage)}
disabled={!selectedImage}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m9 13.5 3 3m0 0 3-3m-3 3v-6m1.06-4.19-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" />
</svg>
</button>
</div>
</div>
);
};
return (
<div >
<div className="z-20" style={{ width: selectedImage ? "100%" : "auto", height: selectedImage ? "100%" : "auto" }}>
<div className='flex justify-center items-center pt-2 pb-20'>
{renderButtons()}
</div>
{selectedImage ? (
<>
<PanZoom
key={selectedImage}
autoCenter={true}
ref={panZoomRef}
>
<div id="image-container" >
<img
src={images[currentIndex]}
style={{ objectFit: "contain", maxWidth: "100%", maxHeight: "calc(100vh - 20px)", pointerEvents: "none" }}
className="cursor-pointer animate-in w-full h-auto"
>
</img>
</div>
</PanZoom>
</>
) : (
<div
className="z-30"
style={{
display: selectedImage ? "flex" : "block",
alignItems: "flex-start",
}}
> <div className='flex justify-center items-center pt-2 '>
<Masonry
breakpointCols={3}
className="my-masonry-grid pl-6 "
style={{ width: selectedImage ? "50%" : "100%" }}
>
{images
.filter((img) => img !== selectedImage)
.map((image, index) => (
<img
key={index}
src={image}
onClick={() => handleClick(image)}
className={`animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse hover:scale-105 p-2 cursor-pointer my-2 transition-all opacity-100 duration-500 ease-in-out transform`}
/>
))}
</Masonry>
</div>
</div>
)}
</div>
</div>
);
}
export default Gallery;

View File

@ -0,0 +1,106 @@
import { useState, useEffect } from 'react';
interface GalleryThumbnailProps {
id: string;
columns: number;
onSelect: (id: string, columns: number) => void;
title: string;
subscription: string;
subscriptionColor: string;
tags: string[];
showNsfw: boolean;
nsfw: boolean;
}
const GalleryThumbnail = ({ id, columns, onSelect, title, showNsfw, nsfw, subscription, subscriptionColor, tags }: GalleryThumbnailProps) => {
const [galleryId, setGalleryId] = useState<string>(id);
const [thumbnailUrl, setThumbnailUrl] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(true);
const [galleryCollumns, setColumns] = useState<number>(columns);
const [imageCount, setImageCount] = useState<number>(0);
const [nsfwState, setNsfw] = useState<boolean>(nsfw);
const [showNsfwState, setShowNsfw] = useState<boolean>(showNsfw);
const [subscriptionState, setSubscription] = useState<string>(subscription);
const [tagsState, setTags] = useState<string[]>(tags);
console.log(subscriptionColor)
const openGallery = () => {
onSelect(galleryId, galleryCollumns);
};
const getData = async () => {
setIsLoading(true);
const thumbnailResponse = await fetch('/api/galleries/' + id + '/thumbnail?nsfw=' + showNsfwState);
const thumbnailUrl = await thumbnailResponse.text();
const imagesCountResponse = await fetch('/api/galleries/' + id + '/images/count');
const imageCount = await imagesCountResponse.json() as number;
setImageCount(imageCount);
setThumbnailUrl(thumbnailUrl);
setIsLoading(false);
};
useEffect(() => {
getData();
}, [galleryId]);
return (
<div className=" py-3 sm:max-w-xl sm:mx-auto flex-3 animate-fade-up animate-once animate-duration-1000 animate-ease-out animate-normal animate-fill-forwards">
<div className="h-48 overflow-visible w-full relative hover:scale-95 rounded-3xl" style={{ cursor: 'pointer'}}>
{!isLoading ? (
<>
<img
className={`aspect-content rounded-3xl shadow-lg`}
src={thumbnailUrl}
alt=""
onClick={openGallery}
key={galleryId}
style={{ width: '20rem', height: '20rem', objectFit: 'cover' }}
/>
<div className="bottom-0 left-0 w-full h-10% p-2 rounded-md flex flex-col justify-end">
<div className="text-white flex justify-between">
<div>
<div className="flex">
<h3 className=" pr-4 text-lg font-bold break-words" style={{ lineHeight: '2rem', textShadow: '0 0 2px black' }}>{title}</h3>
</div>
</div>
</div>
<div className="text-white flex justify-between">
<div className="flex items-center">
<span className="bg-secondary text-white mr-2 px-2 py-1 rounded-md text-sm flex items-center h-full">
<span className="text-center">{imageCount}</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="pl-2 size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
</span>
{nsfwState && (
<span className=" bg-error text-white px-2 py-1 mr-2 rounded-md text-sm h-full flex items-center">NSFW</span>
)}
<span className="text-white px-2 py-1 rounded-md text-sm h-full flex items-center" style={{ cursor: 'pointer', backgroundColor: subscriptionColor }}>Free</span>
</div>
</div>
{/* <div className="text-white flex justify-between">
<div>
<div className="flex">
{tagsState.map((tag, index) => (
<span
key={index}
className={`pr-4 text-sm font-bold break-words"
style={{ lineHeight: '2rem', textShadow: '0 0 2px black' }`}
>
{tag}
</span>
))}
</div>
</div>
</div> */}
</div>
</>
) : (
<div className="animate-pulse bg-secondary-light rounded-3xl" style={{ width: '20rem', height: '20rem' }}></div>
)}
</div>
</div>
);
};
export default GalleryThumbnail;

View File

@ -0,0 +1,149 @@
import { createClient } from "@/utils/supabase/server";
import Link from "next/link";
import { redirect, useRouter } from "next/navigation";
import crypto from 'crypto';
import { headers } from "next/headers";
export default async function AuthButton() {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const signOut = async () => {
"use server";
const supabase = createClient();
await supabase.auth.signOut();
return redirect("/login");
};
const heads = headers()
const currentPage = heads.get('x-path')
const getGravatarUrl = () => {
if (user == null) {
return;
}
let email = user.email;
if (email != null) {
const emailHash = crypto.createHash('md5').update(email.trim().toLowerCase()).digest('hex');
return `https://www.gravatar.com/avatar/${emailHash}`;
}
return "";
}
const url = getGravatarUrl();
const admins = await supabase.from('admins').select('user_id');
let isAdmin = false;
if(!admins.error) {
for (const admin of admins.data) {
if (admin.user_id == user?.id) {
isAdmin = true;
break;
}
}
}
return (
<div className="flex justify-center items-center pt-2 ">
<nav className="w-auto bg-primary bg-opacity-40 flex justify-center z-10 h-16 animate-in rounded-md shadow-lg" style={{ backdropFilter: 'blur(10px)' }}>
<div className="w-auto flex justify-between items-center p-3 text-sm">
<div className="flex items-center gap-2 z-10">
{/* This is admin stuff */}
{(isAdmin) && (
<>
<Link
href="/gallery/admin/"
className={`py-2 px-3 w-38 text-center flex rounded-md no-underline bg-secondary hover:bg-secondary-light`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="lg:hidden size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" />
</svg>
<span className="hidden lg:block">Gallery Admin</span>
</Link>
<Link
href="/admin/"
className={`py-2 px-3 w-38 text-center flex rounded-md no-underline bg-secondary hover:bg-secondary-light`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="lg:hidden size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
<span className="hidden lg:block">System Settings</span>
</Link>
</>
)}
<Link
href="/gallery"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage!="gallery" ? 'bg-primary hover:bg-primary-light' : 'bg-secondary hover:bg-secondary-light'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
<span className="hidden lg:block">Gallery</span>
</Link>
<Link
href="/livestream"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage!="livestream" ? 'bg-primary hover:bg-primary-light' : 'bg-secondary hover:bg-secondary-light'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
<span className="hidden lg:block">Livestream</span>
</Link>
<Link
href="/commissions"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage!="commissions" ? 'bg-primary hover:bg-primary-light' : 'bg-secondary hover:bg-secondary-light'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 1 0-7.5 0v4.5m11.356-1.993 1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 0 1-1.12-1.243l1.264-12A1.125 1.125 0 0 1 5.513 7.5h12.974c.576 0 1.059.435 1.119 1.007ZM8.625 10.5a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm7.5 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
<span className="hidden lg:block">Commissions</span>
</Link>
<Link
href="/subscriptions"
className={`py-2 px-3 flex rounded-md no-underline ${currentPage!="subscriptions" ? 'bg-primary hover:bg-primary-light' : 'bg-secondary hover:bg-secondary-light'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
<span className="hidden lg:block">Subscription</span>
</Link>
</div>
<div className="flex items-center gap-2">
{(user != null) ? (
<>
<form action={signOut}>
<button className="py-2 px-4 ml-2 rounded-md no-underline bg-error hover:bg-error-light">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden ">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 1 1 9 0v3.75M3.75 21.75h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H3.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
<span className="hidden lg:block">Logout</span>
</button>
</form>
<a href="https://gravatar.com/" target="_blank">
<img src={url} alt="Profile" className="w-10 h-10 object-cover rounded-full cursor-pointer" />
</a>
</>
) : (
<Link
href="/subscriptions"
className={`ml-2 py-2 px-3 flex rounded-md no-underline bg-success hover:bg-success-light`}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 lg:hidden block">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
<span className="hidden lg:block">Login</span>
</Link>
)}
</div>
</div>
</nav>
</div>)
}

View File

@ -0,0 +1,16 @@
"use client;"
import React from 'react';
const RightHandLayoutImage: React.FC = () => {
return (
<div className="fixed w-full h-full overflow-hidden z-0 animate-fade-left animate-once animate-duration-[2000ms] animate-normal animate-fill-forwards">
<img
src="/gallery_girl.png"
className="float-right object-cover h-screen w-full lg:w-5/6 xl:w-3/6 opacity-50 overflow-hidden"
alt="Background"
/>
</div>
);
};
export default RightHandLayoutImage;

View File

@ -0,0 +1,60 @@
"use client;"
import React, { useState, useEffect } from 'react';
import SearchInput from '@/components/neroshitron/search_input';
import Galleries from './galleries';
interface SearchProps {
gallerySelected: (gallery: string) => void;
}
const Search = ({ gallerySelected }: SearchProps) => {
const [tags, setTags] = useState<string[]>([]);
const [search, setSearch] = useState<string>('');
const [nsfw, setNsfw] = useState<boolean>(false);
const [gallery, setGallery] = useState<string | null>(null);
const getData = async () => {
}
useEffect(() => {
getData();
}, [search]);
useEffect(() => {
getData();
}, [nsfw]);
useEffect(() => {
getData();
}, [tags]);
useEffect(() => {
getData();
if (gallery != null)
gallerySelected(gallery);
}, [gallery]);
//TRY TESTING WITH THIS REMOVED!
useEffect(() => {
getData();
}, []);
return (
<>
<Galleries gallerySelected={(gallery: string) => { setGallery(gallery) }} key={search + "-" + tags.length + "-" + nsfw} search={search} nsfw={nsfw} tags={tags} />
<section className="fixed flex items-center w-full p-8 pt-20 opacity-90 animate-in animate-once animate-duration-500">
<div className="container mx-auto py-8">
<SearchInput
startingTags={[]}
placeholderTags={[
{ value: "neroshi", label: "🧑‍🎨 neroshi" },
{ value: "neroshi", label: "❗️ click here for tags to search!" },
]} nsfwButtonEnabled={true} searchChanged={(search) => { setSearch(search) }} nsfwChanged={(nsfw) => { setNsfw(nsfw) }} tagsChanged={(tags) => { setTags(tags); }} />
</div>
</section>
</>
);
};
export default Search;

View File

@ -0,0 +1,190 @@
"use client;"
import React, { useState, useEffect, useRef, forwardRef } from 'react';
import TagSelector from '../neroshitron/tag_selector';
import Select from "react-tailwindcss-select";
import { SelectValue } from 'react-tailwindcss-select/dist/components/type';
import { Option } from 'react-tailwindcss-select/dist/components/type';
interface SearchInputProps {
tagsChanged: (tags: string[]) => void;
searchChanged: (search: string) => void;
nsfwChanged: (nsfw: boolean) => void;
nsfwButtonEnabled: boolean | null;
placeholderTags: Option[];
startingTags: string[] | null;
}
const SearchInput = ({ tagsChanged, searchChanged, nsfwChanged, nsfwButtonEnabled, placeholderTags, startingTags }: SearchInputProps) => {
const [tagSearch, setTagSearch] = useState<string>('');
const [nsfw, setNsfw] = useState<boolean>(false);
const [selectedTags, setSelectedTags] = useState<string[]>(startingTags ?? []);
const [selectedTagsInput, setSelectedTagsInput] = useState<Option[]>([...placeholderTags, ...(startingTags ?? []).map((tag) => ({ value: tag, label: tag }))]);
const [selectingTags, setSelectingTags] = useState<boolean>(false);
const tagSelectorRef = React.useRef(null);
const [tags, setTags] = useState<any[]>([]);
const getData = async () => {
const tagsResponse = await fetch(`/api/galleries/tags`);
const tagsData = await tagsResponse.json();
setTags(tagsData);
}
const updateTags = (newTags: string[]) => {
setSelectedTags(newTags)
}
const onTagsClosed = (tags: string[]) => {
setSelectingTags(false);
}
const openTags = () => {
setSelectingTags(true);
if (selectingTags) {
onTagsClosed(selectedTags);
}
}
useEffect(() => {
tagsChanged(selectedTags.filter(tag => tag != "neroshi"));
}, [selectedTags]);
useEffect(() => {
nsfwChanged(nsfw);
}, [nsfw]);
useEffect(() => {
getData();
}, []);
const [color, setColor] = useState('black');
const selectRef = useRef(null);
const [currentTag, setCurrentTag] = useState<string>('');
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp') {
const currentIndex = tags.findIndex(tag => tag.name === currentTag);
const newIndex = currentIndex === 0 ? tags.length - 1 : currentIndex - 1;
setCurrentTag(tags[newIndex].name);
} else if (event.key === 'ArrowDown') {
const currentIndex = tags.findIndex(tag => tag.name === currentTag);
const newIndex = currentIndex === tags.length - 1 ? 0 : currentIndex + 1;
setCurrentTag(tags[newIndex].name);
} else if (event.key === 'Enter') {
const currentIndex = tags.findIndex(tag => tag.name === currentTag);
if (currentIndex !== -1 && !selectedTags.includes(tags[currentIndex].name)) {
setSelectedTags([...selectedTags, tags[currentIndex].name]);
const tagsInput = selectedTagsInput;
tagsInput.push({ value: tags[currentIndex].name, label: tags[currentIndex].name });
setSelectedTagsInput(tagsInput);
setCurrentTag('');
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [currentTag, tags]);
const tagOptions = tags.map((tag: { name: string; }) => ({ value: tag.name, label: tag.name }));
return (
<>
<div className={` opacity 0 relative w-full flex flex-col items-center justify-center z-10`}>
<div className="search-box mx-auto my-auto w-full">
<div className={`flex flex-row`}>
{(selectingTags) ? (
<>
<input autoFocus value={tagSearch} onChange={(e) => setTagSearch(e.target.value)} className="rounded-l-md h-16 bg-gray-100 text-grey-darker py-2 font-normal text-grey-darkest border border-gray-100 font-bold w-full py-1 px-2 outline-none text-lg text-gray-600" type="text" placeholder="Looking for specific tag?" />
<span className="flex items-center bg-gray-100 rounded rounded-l-none border-0 px-3 font-bold text-grey-100">
<button key="back" onClick={() => { openTags() }} type="button" className={`animate-in bg-pink-900 hover:bg-pink-800 text-lg text-white font-bold py-3 px-6 rounded`}>
Back
</button>
</span>
</>
)
: (
<>
<div className="w-full top-0">
<Select isMultiple isSearchable isClearable searchInputPlaceholder='Start typing to search tags...'
options={tagOptions}
placeholder="Select tags for your search"
onChange={(value: Option | Option[] | null) => {
if (value === null) {
setSelectedTags([]);
setSelectedTagsInput([]);
}
if (Array.isArray(value)) {
setSelectedTags(value.map((option) => option.value));
setSelectedTagsInput(value as Option[])
} else if (value) {
setSelectedTags([value.value]);
setSelectedTagsInput([value])
}
if (Array.isArray(value)) {
setSelectedTags(value.map((option) => option.value));
setSelectedTagsInput(value.filter((option) => option.value !== 'placeholder'));
} else if (value) {
setSelectedTags([value.value]);
setSelectedTagsInput([value]);
}
}}
onSearchInputChange={(value) => {
}}
classNames={{
menu: "bg-secondary-dark text-white pb-4 rounded",
searchBox: "rounded-md bg-secondary w-1/2 text-white w-full mt-2 p-2 mb-2 animate-in",
searchIcon: "hidden",
tagItem: (value) => "hover:scale-95 bg-primary-light rounded-md pl-2 p-1 m-1 flex",
tagItemText: "text-white animate-in",
closeIcon: "text-white",
tagItemIconContainer:"animate-in"
}}
formatOptionLabel={data => (
<li key={"tag-" + data.value}
className={`animate-in block transition rounded duration-200 px-2 py-2 cursor-pointer select-none truncate pt-2 bg-primary text-white ${currentTag==data.value ? "bg-primary-light" : ""} hover:bg-primary-light
}`}
>
{data.label}
</li>
)}
value={selectedTagsInput}
primaryColor={"indigo"} />
</div>
{(nsfwButtonEnabled) && (
<span className="w-1/6 border-0 font-bold text-grey-100">
<button
onClick={() => { setNsfw(!nsfw) }}
type="button"
className={`animate-in text-sm text-white font-bold h-full w-16 px-2 rounded rounded-l-none ${nsfw ? "bg-error hover:bg-error-light" : "bg-success hover:bg-success-light"}`}
>
{nsfw ? "NSFW" : "SFW"}
</button>
</span>
)}
</>
)}
</div>
</div>
</div>
{(selectingTags) &&
<TagSelector key={tagSearch} tagSearch={tagSearch} tagsChanged={(newTags: string[]) => { updateTags(newTags) }} selectedTagsInput={selectedTags} ref={tagSelectorRef} />}
</>
);
};
export default SearchInput;

View File

@ -0,0 +1,20 @@
"use client;"
import React, { useState, useEffect } from 'react';
interface TagProps { onTagClicked: (tag: string) => void, selected: boolean, tag: string }
const Tag = ({ onTagClicked, selected, tag, }: TagProps) => {
return (
<button
key={tag}
type="button"
className={`animate-in w-full h-8 rounded-md no-underline text-sm text-white py-1 font-medium text-center ${selected ? 'hover:bg-pink-800 bg-pink-900' : 'hover:bg-pink-600 bg-neroshi-blue-800 border-neroshi-blue-900 border-2'}`}
onClick={() => onTagClicked(tag)}
>
{tag}
</button>
);
};
export default Tag;

View File

@ -0,0 +1,61 @@
"use client;"
import React, { forwardRef, useState, useEffect } from 'react';
import Tag from './tag_pill';
interface TagSelectorProps {
tagSearch: string,
selectedTagsInput: string[],
tagsChanged: (tags: string[]) => void
}
const TagSelector = forwardRef<TagSelectorProps, { tagSearch: string, selectedTagsInput: string[], tagsChanged: (tags: string[]) => void }>((props, ref) => {
const [tags, setTags] = useState<any[]>([]);
const [tagSearch, setTagSearch] = useState<string>(props.tagSearch);
const [selectedTags, setSelectedTags] = useState<string[]>(props.selectedTagsInput);
const handleTag = (tag: string) => {
if (selectedTags.includes(tag)) {
setSelectedTags(selectedTags.filter(t => t !== tag));
} else {
setSelectedTags([...selectedTags, tag]);
}
setTags(selectedTags);
};
const getData = async () => {
const tagsResponse = await fetch(`/api/galleries/tags`);
const tagsData = await tagsResponse.json();
setTags(tagsData);
}
useEffect(() => {
props.tagsChanged(selectedTags);
getData();
}, [selectedTags, tagSearch]);
useEffect(() => {
props.tagsChanged(selectedTags);
getData();
}, [selectedTags, tagSearch]);
useEffect(() => {
getData();
}, []);
return (
<div className="flex md:w-full pt-4 justify-center items-center">
{(tags.length > 0) && (
<div className="z-10 grid p-4 grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-1 w-full max-h-96 overflow-y-scroll no-scrollbar pt-4 bg-neroshi-blue-900 rounded-md opacity-90 backdrop-filter backdrop-blur-md mx-auto">
{tags.map((tag: any) => (
(tagSearch === '' || tag.name.toLowerCase().includes(tagSearch.toLowerCase())) && // Updated condition
<Tag tag={tag.name} selected={selectedTags.includes(tag.name)} onTagClicked={(tag) => handleTag(tag)} />
))}
</div>
)}
</div>
);
});
export default TagSelector;

37
components/theme.tsx Normal file
View File

@ -0,0 +1,37 @@
"use client";
import { useEffect, useState } from 'react';
import { createClient } from '@/utils/supabase/client';
interface GalleryThumbnailProps {}
const ThemeProvider = ({}: GalleryThumbnailProps) => {
const [data, setData] = useState<any[]>([]); // State to store the fetched data
const getData = async () => {
const supabase = createClient();
const { data, error } = await supabase.from('interface_configurations').select('*');
if (error) {
console.error('Error fetching data:', error);
} else {
setData(data || []);
}
};
useEffect(() => {
getData();
}, []);
useEffect(() => {
// Update variables when data changes
for (const config of data) {
if (config.type === 'color') {
document.documentElement.style.setProperty(`--color-${config.name}`, config.value);
}
}
}, [data]);
return <></>;
};
export default ThemeProvider;

View File

@ -1,218 +0,0 @@
import { use, useState } from 'react';
import { useEffect } from 'react';
import { render } from 'react-dom';
import Masonry from 'react-masonry-css';
import PanZoom from 'react-easy-panzoom';
interface GalleryProps {
id: string;
columns: number;
closeMenu: () => void;
}
const Gallery = ({ id, columns, closeMenu }: GalleryProps) => {
const [isSingle, setIsSingle] = useState<boolean>(false);
const [loaded, setLoaded] = useState({})
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [images, setImages] = useState<string[]>([]);
const [galleryId, setGalleryId] = useState(id as string);
const [currentIndex, setCurrentIndex] = useState(0);
const getData = async () => {
const thumbnailResponse = await fetch('/api/galleries/' + String(galleryId) + '/images');
const thumbnailUrl = await thumbnailResponse.json() as string[];
setImages(thumbnailUrl);
}
const next = () => {
if (currentIndex < images.length - 1) {
setCurrentIndex(currentIndex + 1);
} else {
setCurrentIndex(0);
}
}
const previous = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
} else {
setCurrentIndex(images.length - 1);
}
}
const renderButtons = () => {
return (
<>
<div className="z-20 fixed left-6 bottom-4 w-100 h-20 bg-purple-900 bg-opacity-40 animate-in rounded-3xl" style={{ backdropFilter: 'blur(10px)' }}>
<div className='grid grid-cols-4 gap-4 pl-32 pr-4 pt-4 m-1 '>
<button
className={`justify-center text-center w-full animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50 ${!selectedImage ? 'opacity-50 cursor-not-allowed bg-gray-800' : 'bg-pink-700 hover:bg-pink-600'}`}
onClick={() => previous()}
disabled={!selectedImage}
>
Previous
</button>
<button
className={`justify-center text-center w-full animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50 ${!selectedImage ? 'opacity-50 cursor-not-allowed bg-gray-800' : 'bg-pink-700 hover:bg-pink-600'}`}
onClick={() => next()}
disabled={!selectedImage}
>
Next
</button>
<button
className={`justify-center text-center w-full animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50 ${!selectedImage ? 'opacity-50 cursor-not-allowed bg-gray-800' : 'bg-purple-700 hover:bg-purple-600'}`}
onClick={() => selectedImage && handleDownload(selectedImage)}
disabled={!selectedImage}
>
Download
</button>
<button
className={`justify-center text-center w-full animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50 ${!selectedImage ? 'opacity-50 cursor-not-allowed bg-gray-800' : 'bg-purple-700 hover:bg-purple-600'}`}
onClick={() => selectedImage && open()}
disabled={!selectedImage}
>
Open
</button>
</div>
</div>
</>
);
};
const handleDownload = (image: string) => {
const link = document.createElement('a');
link.href = image;
link.download = 'image.jpg'; // or any other filename
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
useEffect(() => {
getData();
const handleKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowLeft':
case 'a':
case 'A':
previous();
break;
case 'ArrowRight':
case 'd':
case 'D':
next();
break;
case 'Escape':
close();
break;
default:
break;
}
};
setSelectedImage(images[currentIndex]);
window.addEventListener('keydown', handleKeyDown);
// Clean up the event listener when the component is unmounted
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [selectedImage,currentIndex]);
const handleClick = (image: string) => {
setSelectedImage(image);
setCurrentIndex(images.indexOf(image));
};
const open = () => {
if (selectedImage === null) return;
console.log(selectedImage)
let base64Image = selectedImage.split(';base64,').pop();
if (!base64Image) return;
let blob = new Blob([Uint8Array.from(atob(base64Image), c => c.charCodeAt(0))], { type: 'image/jpeg' }); // adjust the type as needed
let url = URL.createObjectURL(blob);
window.open(url, '_blank');
}
const close = () => {
if (selectedImage != null) {
setSelectedImage(null);
setImages([]);
}
else {
closeMenu();
}
}
const breakpointColumnsObj = {
default: 3
};
return (
<div className="z-20">
<button
className="fixed bg-purple-800 hover:bg-purple-700 left-10 bottom-5 animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse mb-4 py-2 px-4 rounded-lg no-underline flex items-center z-50"
onClick={() => close()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1"
>
<polyline points="15 18 9 12 15 6" />
</svg>{" "}
Back
</button>
{renderButtons()}
{selectedImage ? (<PanZoom>
<img
src={images[currentIndex]}
style={{ objectFit: "contain", maxWidth: "100%", maxHeight: "calc(100vh - 20px)", pointerEvents:"none" }}
className="cursor-pointer animate-in w-full h-auto"
onClick={() => close()}
/>
</PanZoom>
) : (
<div
className="z-30 pb-10"
style={{
display: selectedImage ? "flex" : "block",
alignItems: "flex-start",
}}
>
<Masonry
breakpointCols={columns}
className="my-masonry-grid"
style={{ width: selectedImage ? "50%" : "100%" }}
>
{images
.filter((img) => img !== selectedImage)
.map((image, index) => (
<img
src={image}
onClick={() => handleClick(image)}
className={`animate-in animate-once animate-duration-1000 animate-ease-out animate-reverse hover:scale-105 p-2 cursor-pointer my-2 transition-all opacity-100 duration-500 ease-in-out transform`}
/>
))}
</Masonry>
<>
</>
</div>
)}
</div>
);
}
export default Gallery;

View File

@ -1,113 +0,0 @@
import { useState, useEffect } from 'react';
interface GalleryThumbnailProps {
id: string;
columns: number;
onSelect: (id: string, columns: number) => void;
title: string;
subscription: string;
}
const GalleryThumbnail = ({ id, columns, onSelect, title, subscription }: GalleryThumbnailProps) => {
const [galleryId, setGalleryId] = useState<string>(id);
const [thumbnailUrl, setThumbnailUrl] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(true);
const [galleryCollumns, setColumns] = useState<number>(columns);
const [imageCount, setImageCount] = useState<number>(0);
const openGallery = () => {
onSelect(galleryId, galleryCollumns);
};
const getData = async () => {
setIsLoading(true);
const thumbnailResponse = await fetch('/api/galleries/' + galleryId + '/thumbnail');
const thumbnailUrl = await thumbnailResponse.text();
const imagesCountResponse = await fetch('/api/galleries/' + galleryId + '/images/count');
const imageCount = await imagesCountResponse.json() as number;
setImageCount(imageCount);
setThumbnailUrl(thumbnailUrl);
setIsLoading(false);
};
useEffect(() => {
getData();
}, []);
return (
<div className="py-3 sm:max-w-xl sm:mx-auto flex-3 animate-in">
<div className="h-48 overflow-visible w-full relative hover:scale-105 shadow-lg bg-gray-400 rounded-3xl">
{!isLoading && (
<>
<img
className={`aspect-content rounded-3xl`}
src={thumbnailUrl}
alt=""
onClick={openGallery}
style={{ width: '20rem', height: '20rem', objectFit: 'cover' }}
/>
<div className="absolute top-2 left-0 w-full h-10% bg-gray-900 bg-opacity-10 backdrop-blur-sm p-2 rounded-md shadow-lg flex flex-col justify-end">
<div className="text-white flex justify-between">
<div>
<div className="flex">
<h3 className="pr-4 text-lg font-bold break-words" style={{ lineHeight: '2rem', textShadow: '0 0 2px black' }}>{title}</h3>
</div>
</div>
<div className="flex items-center">
<div className="relative group">
<div className="absolute top-0 right-2 mt-12 w-48 px-2 py-1 bg-black text-white text-xs rounded hidden group-hover:block">
<div className="text-center">{imageCount} pictures in this gallery.</div>
</div>
<span className="bg-neroshi-blue-900 text-white mr-2 px-2 py-1 rounded-md text-sm flex items-center h-full">
<span className="text-center">{imageCount}</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="pl-2 size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
</span>
<div className="absolute top-0 right-2 mt-12 w-48 px-2 py-1 bg-black text-white text-xs rounded hidden group-hover:block">
<div className="text-center">{imageCount} pictures in this gallery.</div>
</div>
</div>
{subscription === "Free" && (
<div className="relative group">
<span className="bg-gray-900 text-white px-2 py-1 rounded-md text-sm h-full flex items-center">Free</span>
<div className="absolute top-0 right-12 mt-12 w-48 px-2 py-1 bg-black text-white text-xs rounded hidden group-hover:block">
<div className="text-center">This is free for everyone.</div>
</div>
</div>
)}
{subscription === "Tier 1" && (
<div className="relative group">
<span className="bg-purple-600 text-white px-2 py-1 rounded-md text-sm h-full flex items-center">Tier 1</span>
<div className="absolute top-0 right-12 mt-12 w-48 px-2 py-1 bg-black text-white text-xs rounded hidden group-hover:block">
<div className="text-center">This requires a Tier 1 subscription.</div>
</div>
</div>
)}
{subscription === "Tier 2" && (
<div className="relative group">
<span className="bg-pink-700 text-white px-2 py-1 rounded-md text-sm h-full flex items-center">Tier 2</span>
<div className="absolute top-0 right-12 mt-12 w-48 px-2 py-1 bg-black text-white text-xs rounded hidden group-hover:block">
<div className="text-center">This requires a Tier 2 subscription.</div>
</div>
</div>
)}
{subscription === "Tier 3" && (
<div className="relative group">
<span className="bg-fuchsia-500 text-white px-2 py-1 rounded-md text-sm h-full flex items-center">Tier 3</span>
<div className="absolute top-0 right-12 mt-12 w-48 px-2 py-1 bg-black text-white text-xs rounded hidden group-hover:block">
<div className="text-center">This requires a Tier 3 subscription.</div>
</div>
</div>
)}
</div>
</div>
</div>
</>
)}
</div>
</div>
);
};
export default GalleryThumbnail;

View File

@ -5,8 +5,6 @@ x-logging: &x-logging
options: options:
max-file: '5' max-file: '5'
max-size: '10m' max-size: '10m'
include:
- "supabase/docker-compose.yml"
services: services:
# neroshitron: # neroshitron:
# build: # build:
@ -23,15 +21,6 @@ services:
- 1935:1935 - 1935:1935
volumes: volumes:
- ./data:/owncast/data - ./data:/owncast/data
maildev:
image: maildev/maildev:latest
restart: unless-stopped
ports:
- 1080:1080
- 1025:1025
volumes:
- ./data:/maildev/data
volumes: volumes:
db-config: db-config:

503
docs/.$diagrams.drawio.bkp Normal file
View File

@ -0,0 +1,503 @@
<mxfile host="Electron" modified="2024-06-04T04:27:05.636Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.2.5 Chrome/120.0.6099.109 Electron/28.1.0 Safari/537.36" etag="x7V6P6bjO-UHE86F-Cl3" version="24.2.5" type="device">
<diagram name="Page-1" id="F3YAVjulPUqdYhbqfjdd">
<mxGraphModel dx="1877" dy="2420" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="9zziB1Dtd-V9IfowJO3V-28" value="&lt;h1&gt;Database Design&lt;/h1&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="332.5" y="-70" width="390" height="10" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-29" value="&lt;h1&gt;UX Flow&lt;/h1&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="30" y="620" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-37" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-33" target="9zziB1Dtd-V9IfowJO3V-36" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-33" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
<mxGeometry x="35" y="690" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-39" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-36" target="9zziB1Dtd-V9IfowJO3V-38" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-36" value="Open Site" style="whiteSpace=wrap;html=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="105" y="705" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-38" target="9zziB1Dtd-V9IfowJO3V-41" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-43" value="No" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-42" vertex="1" connectable="0">
<mxGeometry x="0.2533" y="2" relative="1" as="geometry">
<mxPoint x="2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-48" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-38" target="9zziB1Dtd-V9IfowJO3V-47" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-72" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-48" vertex="1" connectable="0">
<mxGeometry x="-0.2273" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-73" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-38" target="9zziB1Dtd-V9IfowJO3V-71" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="265" y="740" />
<mxPoint x="265" y="795" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-74" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-73" vertex="1" connectable="0">
<mxGeometry x="0.0411" y="1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-82" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-38" target="9zziB1Dtd-V9IfowJO3V-81" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="305" y="890" />
<mxPoint x="93" y="890" />
<mxPoint x="93" y="940" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-83" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-82" vertex="1" connectable="0">
<mxGeometry x="-0.8649" y="-1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-92" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-38" target="9zziB1Dtd-V9IfowJO3V-91" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-93" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-92" vertex="1" connectable="0">
<mxGeometry x="-0.6335" y="-2" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-38" value="&lt;br&gt;Logged&lt;br&gt;In" style="rhombus;whiteSpace=wrap;html=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="265" y="680" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-46" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-41" target="9zziB1Dtd-V9IfowJO3V-45" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-41" value="Login/Signup Page" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#a20025;strokeColor=#6F0000;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="245" y="620" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-49" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-45" target="9zziB1Dtd-V9IfowJO3V-47" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-50" value="Click Activation&lt;br&gt;Email Link" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-49" vertex="1" connectable="0">
<mxGeometry x="0.1683" y="1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-45" value="Send Confirmation Email" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#a20025;strokeColor=#6F0000;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="235" y="560" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-52" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-47" target="9zziB1Dtd-V9IfowJO3V-51" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="540" y="716.31" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-47" value="Gallery Page" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="440" y="705" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-54" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-51" target="9zziB1Dtd-V9IfowJO3V-53" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-51" value="Search By Title &amp;amp;&lt;br&gt;Filter By Tag" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="642" y="698.02" width="120" height="45" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-55" value="Refine Search" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-53" target="9zziB1Dtd-V9IfowJO3V-51" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-57" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-53" target="9zziB1Dtd-V9IfowJO3V-56" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-53" value="Browse Galleries" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="802" y="704.27" width="120" height="32.5" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-59" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-56" target="9zziB1Dtd-V9IfowJO3V-58" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-56" value="Open Gallery" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="802" y="780.52" width="120" height="32.5" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-61" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-58" target="9zziB1Dtd-V9IfowJO3V-60" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-63" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-58" target="9zziB1Dtd-V9IfowJO3V-62" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-65" value="Directional&lt;br&gt;Buttons" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-63" vertex="1" connectable="0">
<mxGeometry x="0.1714" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-69" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.25;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-58" target="9zziB1Dtd-V9IfowJO3V-68" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="742" y="850.52" />
<mxPoint x="742" y="840.52" />
<mxPoint x="732" y="840.52" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-58" value="Enlarge Image &amp;amp;&amp;nbsp;&lt;br&gt;Pan/Zoom" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="802" y="840.52" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-60" value="Download Image" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="802" y="900.52" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-64" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-62" target="9zziB1Dtd-V9IfowJO3V-58" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="672" y="960.52" />
<mxPoint x="782" y="960.52" />
<mxPoint x="782" y="870.52" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-62" value="Next/Previous&lt;br&gt;Image" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="612" y="890.52" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-70" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-68" target="9zziB1Dtd-V9IfowJO3V-56" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="762" y="797.39" as="sourcePoint" />
<mxPoint x="792" y="796.14" as="targetPoint" />
<Array as="points">
<mxPoint x="762" y="796.52" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-68" value="Close Image" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="642" y="787.52" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-76" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-71" target="9zziB1Dtd-V9IfowJO3V-75" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-78" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-71" target="9zziB1Dtd-V9IfowJO3V-77" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-71" value="Livestream Page" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#d80073;strokeColor=#A50040;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="125" y="780" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-75" value="Watch Stream" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#d80073;strokeColor=#A50040;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="125" y="840" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-77" value="Chat" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#d80073;strokeColor=#A50040;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="12.5" y="780" width="75" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-85" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" source="9zziB1Dtd-V9IfowJO3V-81" target="9zziB1Dtd-V9IfowJO3V-84" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-87" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" source="9zziB1Dtd-V9IfowJO3V-81" target="9zziB1Dtd-V9IfowJO3V-86" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-81" value="Subscriptions Page" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="32.5" y="940" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-84" value="View Current Tier" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="32.5" y="990" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-89" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" source="9zziB1Dtd-V9IfowJO3V-86" target="9zziB1Dtd-V9IfowJO3V-88" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-86" value="View Available Tiers" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="187.5" y="940" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-90" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" source="9zziB1Dtd-V9IfowJO3V-88" target="9zziB1Dtd-V9IfowJO3V-84" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-88" value="Upgrade Tier" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="187.5" y="990" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-95" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-91" target="9zziB1Dtd-V9IfowJO3V-94" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="410" y="810" />
<mxPoint x="360" y="810" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-100" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-91" target="9zziB1Dtd-V9IfowJO3V-99" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-91" value="Commissions Page" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="350" y="770" width="120" height="27.48" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-97" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-94" target="9zziB1Dtd-V9IfowJO3V-96" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-98" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-97" vertex="1" connectable="0">
<mxGeometry x="-0.6109" relative="1" as="geometry">
<mxPoint x="20" y="2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-105" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-94" target="9zziB1Dtd-V9IfowJO3V-110" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-106" value="No" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-105" vertex="1" connectable="0">
<mxGeometry x="-0.1779" y="-2" relative="1" as="geometry">
<mxPoint y="-2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-94" value="Has Existing&lt;br&gt;Commission" style="rhombus;whiteSpace=wrap;html=1;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="310" y="830.52" width="100" height="100" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-101" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-96" target="9zziB1Dtd-V9IfowJO3V-99" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-96" value="View Existing&lt;br&gt;Commission Status" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="350" y="970" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-99" value="View All Commissions" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="500" y="770" width="130" height="27.48" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-109" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-104" target="9zziB1Dtd-V9IfowJO3V-96" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="560" y="985" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-104" value="Request New&lt;br&gt;Commission" style="whiteSpace=wrap;html=1;fillColor=#60a917;strokeColor=#2D7600;rounded=0;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="470" y="817.52" width="120" height="39.48" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-111" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-110" target="9zziB1Dtd-V9IfowJO3V-96" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="440" y="930" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-112" value="No" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-111" vertex="1" connectable="0">
<mxGeometry x="0.0044" relative="1" as="geometry">
<mxPoint y="5" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-113" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-110" target="9zziB1Dtd-V9IfowJO3V-104" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-114" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-113" vertex="1" connectable="0">
<mxGeometry x="-0.5043" y="1" relative="1" as="geometry">
<mxPoint x="3" y="-1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-110" value="Requests&lt;br&gt;Open" style="rhombus;whiteSpace=wrap;html=1;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="440" y="860.52" width="79.48" height="79.48" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-5" value="public.tags" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="207.5" y="440" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-6" value="name" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-5">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-9" value="public.admins" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="243" y="60" width="175" height="120" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-10" value="PK FK Unique uuid user_id" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-9">
<mxGeometry y="30" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-11" value="assigned_date:date" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-9">
<mxGeometry y="60" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-12" value="assigner_id:uuid?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-9">
<mxGeometry y="90" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-17" value="auth.users" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" vertex="1" parent="1">
<mxGeometry x="10" y="120" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-18" value="id" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-17">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#0050ef;strokeColor=#001DBC;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-18" target="1AZqCnQGpeGdDfHHl4o8-10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#0050ef;strokeColor=#001DBC;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-18" target="1AZqCnQGpeGdDfHHl4o8-14">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-23" value="public.tier" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#a20025;fontColor=#ffffff;strokeColor=#6F0000;" vertex="1" parent="1">
<mxGeometry x="32.5" y="290" width="140" height="120" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-24" value="PK name:text" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-23">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-25" value="price:decimal" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-23">
<mxGeometry y="60" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-26" value="stripe_product_id:string" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-23">
<mxGeometry y="90" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-29" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#a20025;strokeColor=#6F0000;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-24" target="1AZqCnQGpeGdDfHHl4o8-15">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-34" value="public.galleries" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="497.5" y="320" width="140" height="180" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-35" value="PK string name" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-34">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-37" value="FK tier string" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-34">
<mxGeometry y="60" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-38" value="string thumbnail_file" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-34">
<mxGeometry y="90" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-41" value="bool nsfw" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-34">
<mxGeometry y="120" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-39" value="text[] tags" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-34">
<mxGeometry y="150" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-42" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#a20025;strokeColor=#6F0000;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-24" target="1AZqCnQGpeGdDfHHl4o8-37">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="207.5" y="335" />
<mxPoint x="207.5" y="395" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-43" value="public.skibs" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="499.5" width="140" height="300" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-44" value="PK AI id:&amp;nbsp;int8" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-46" value="amount:decimal" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="60" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-49" value="user_id:uuid" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="90" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-50" value="request_date:date" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="120" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-51" value="accepted:bool?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="150" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-58" value="processed_date:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="180" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-52" value="payment_url:string" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="210" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-57" value="paidDate:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="240" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-59" value="completedDate:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="270" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-48" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;startArrow=classic;startFill=1;fillColor=#1ba1e2;strokeColor=#006EAF;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-6" target="1AZqCnQGpeGdDfHHl4o8-39">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-56" value="No FK Relationship" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontColor=#FFFFFF;labelBackgroundColor=none;" vertex="1" connectable="0" parent="1AZqCnQGpeGdDfHHl4o8-48">
<mxGeometry x="-0.0082" y="2" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-13" value="public.user_subscriptions" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="243" y="203" width="175" height="180" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-14" value="PK FK Unique user_id:uuid" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-13">
<mxGeometry y="30" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-15" value="FK tier:text" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-13">
<mxGeometry y="60" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-54" value="active:bool" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-13">
<mxGeometry y="90" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-53" value="last_paid_date:date" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-13">
<mxGeometry y="120" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-47" value="start_date:date" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-13">
<mxGeometry y="150" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-55" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#0050ef;strokeColor=#001DBC;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-18" target="1AZqCnQGpeGdDfHHl4o8-49">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="197.5" y="165" />
<mxPoint x="197.5" y="30" />
<mxPoint x="447.5" y="30" />
<mxPoint x="447.5" y="105" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-60" value="public.commissions" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="697.5" y="30" width="170" height="330" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-61" value="PK AI id:&amp;nbsp;int8" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="30" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-62" value="amount:decimal" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="60" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-63" value="user_id:uuid" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="90" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-64" value="request_date:date" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="120" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-65" value="accepted:bool?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="150" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-66" value="processed_date:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="180" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-67" value="hours:number" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="210" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-70" value="pending_approval:bool false" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="240" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-68" value="paidDate:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="270" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-69" value="completedDate:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="300" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-71" value="public.comission_messages" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="957.5" y="180" width="210" height="120" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-72" value="PK AI id:int8" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-71">
<mxGeometry y="30" width="210" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-73" value="FK comission_id:int8" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-71">
<mxGeometry y="60" width="210" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-74" value="FK sender_id:uuid" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-71">
<mxGeometry y="90" width="210" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-79" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#0050ef;strokeColor=#001DBC;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-18" target="1AZqCnQGpeGdDfHHl4o8-63">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="197.5" y="165" />
<mxPoint x="197.5" y="30" />
<mxPoint x="447.5" y="30" />
<mxPoint x="447.5" y="-30" />
<mxPoint x="667.5" y="-30" />
<mxPoint x="667.5" y="135" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-80" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#1ba1e2;strokeColor=#006EAF;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-61" target="1AZqCnQGpeGdDfHHl4o8-73">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-81" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#0050ef;strokeColor=#001DBC;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-18" target="1AZqCnQGpeGdDfHHl4o8-74">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="197.5" y="165" />
<mxPoint x="197.5" y="30" />
<mxPoint x="447.5" y="30" />
<mxPoint x="447.5" y="-30" />
<mxPoint x="667.5" y="-30" />
<mxPoint x="667.5" y="400" />
<mxPoint x="897.5" y="400" />
<mxPoint x="897.5" y="285" />
</Array>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

503
docs/diagrams.drawio Normal file
View File

@ -0,0 +1,503 @@
<mxfile host="Electron" modified="2024-06-04T04:27:06.049Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/24.2.5 Chrome/120.0.6099.109 Electron/28.1.0 Safari/537.36" etag="nuI_NnP1br-4Uuhgp7xU" version="24.2.5" type="device">
<diagram name="Page-1" id="F3YAVjulPUqdYhbqfjdd">
<mxGraphModel dx="1877" dy="2420" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="9zziB1Dtd-V9IfowJO3V-28" value="&lt;h1&gt;Database Design&lt;/h1&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="332.5" y="-70" width="390" height="10" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-29" value="&lt;h1&gt;UX Flow&lt;/h1&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
<mxGeometry x="30" y="620" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-37" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-33" target="9zziB1Dtd-V9IfowJO3V-36" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-33" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" parent="1" vertex="1">
<mxGeometry x="35" y="690" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-39" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-36" target="9zziB1Dtd-V9IfowJO3V-38" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-36" value="Open Site" style="whiteSpace=wrap;html=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="105" y="705" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-38" target="9zziB1Dtd-V9IfowJO3V-41" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-43" value="No" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-42" vertex="1" connectable="0">
<mxGeometry x="0.2533" y="2" relative="1" as="geometry">
<mxPoint x="2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-48" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-38" target="9zziB1Dtd-V9IfowJO3V-47" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-72" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-48" vertex="1" connectable="0">
<mxGeometry x="-0.2273" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-73" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-38" target="9zziB1Dtd-V9IfowJO3V-71" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="265" y="740" />
<mxPoint x="265" y="795" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-74" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-73" vertex="1" connectable="0">
<mxGeometry x="0.0411" y="1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-82" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-38" target="9zziB1Dtd-V9IfowJO3V-81" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="305" y="890" />
<mxPoint x="93" y="890" />
<mxPoint x="93" y="940" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-83" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-82" vertex="1" connectable="0">
<mxGeometry x="-0.8649" y="-1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-92" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-38" target="9zziB1Dtd-V9IfowJO3V-91" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-93" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-92" vertex="1" connectable="0">
<mxGeometry x="-0.6335" y="-2" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-38" value="&lt;br&gt;Logged&lt;br&gt;In" style="rhombus;whiteSpace=wrap;html=1;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="265" y="680" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-46" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-41" target="9zziB1Dtd-V9IfowJO3V-45" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-41" value="Login/Signup Page" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#a20025;strokeColor=#6F0000;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="245" y="620" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-49" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-45" target="9zziB1Dtd-V9IfowJO3V-47" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-50" value="Click Activation&lt;br&gt;Email Link" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-49" vertex="1" connectable="0">
<mxGeometry x="0.1683" y="1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-45" value="Send Confirmation Email" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#a20025;strokeColor=#6F0000;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="235" y="560" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-52" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-47" target="9zziB1Dtd-V9IfowJO3V-51" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="540" y="716.31" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-47" value="Gallery Page" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="440" y="705" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-54" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-51" target="9zziB1Dtd-V9IfowJO3V-53" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-51" value="Search By Title &amp;amp;&lt;br&gt;Filter By Tag" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="642" y="698.02" width="120" height="45" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-55" value="Refine Search" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-53" target="9zziB1Dtd-V9IfowJO3V-51" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-57" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-53" target="9zziB1Dtd-V9IfowJO3V-56" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-53" value="Browse Galleries" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="802" y="704.27" width="120" height="32.5" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-59" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-56" target="9zziB1Dtd-V9IfowJO3V-58" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-56" value="Open Gallery" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="802" y="780.52" width="120" height="32.5" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-61" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-58" target="9zziB1Dtd-V9IfowJO3V-60" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-63" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-58" target="9zziB1Dtd-V9IfowJO3V-62" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-65" value="Directional&lt;br&gt;Buttons" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-63" vertex="1" connectable="0">
<mxGeometry x="0.1714" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-69" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.25;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-58" target="9zziB1Dtd-V9IfowJO3V-68" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="742" y="850.52" />
<mxPoint x="742" y="840.52" />
<mxPoint x="732" y="840.52" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-58" value="Enlarge Image &amp;amp;&amp;nbsp;&lt;br&gt;Pan/Zoom" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="802" y="840.52" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-60" value="Download Image" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="802" y="900.52" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-64" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-62" target="9zziB1Dtd-V9IfowJO3V-58" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="672" y="960.52" />
<mxPoint x="782" y="960.52" />
<mxPoint x="782" y="870.52" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-62" value="Next/Previous&lt;br&gt;Image" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="612" y="890.52" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-70" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-68" target="9zziB1Dtd-V9IfowJO3V-56" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="762" y="797.39" as="sourcePoint" />
<mxPoint x="792" y="796.14" as="targetPoint" />
<Array as="points">
<mxPoint x="762" y="796.52" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-68" value="Close Image" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#1ba1e2;strokeColor=#006EAF;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="642" y="787.52" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-76" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-71" target="9zziB1Dtd-V9IfowJO3V-75" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-78" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-71" target="9zziB1Dtd-V9IfowJO3V-77" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-71" value="Livestream Page" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#d80073;strokeColor=#A50040;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="125" y="780" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-75" value="Watch Stream" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#d80073;strokeColor=#A50040;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="125" y="840" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-77" value="Chat" style="whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#d80073;strokeColor=#A50040;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="12.5" y="780" width="75" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-85" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" source="9zziB1Dtd-V9IfowJO3V-81" target="9zziB1Dtd-V9IfowJO3V-84" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-87" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" source="9zziB1Dtd-V9IfowJO3V-81" target="9zziB1Dtd-V9IfowJO3V-86" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-81" value="Subscriptions Page" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="32.5" y="940" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-84" value="View Current Tier" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="32.5" y="990" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-89" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" source="9zziB1Dtd-V9IfowJO3V-86" target="9zziB1Dtd-V9IfowJO3V-88" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-86" value="View Available Tiers" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="187.5" y="940" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-90" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" source="9zziB1Dtd-V9IfowJO3V-88" target="9zziB1Dtd-V9IfowJO3V-84" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-88" value="Upgrade Tier" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#6a00ff;strokeColor=#3700CC;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="187.5" y="990" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-95" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-91" target="9zziB1Dtd-V9IfowJO3V-94" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="410" y="810" />
<mxPoint x="360" y="810" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-100" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-91" target="9zziB1Dtd-V9IfowJO3V-99" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-91" value="Commissions Page" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="350" y="770" width="120" height="27.48" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-97" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="9zziB1Dtd-V9IfowJO3V-94" target="9zziB1Dtd-V9IfowJO3V-96" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-98" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-97" vertex="1" connectable="0">
<mxGeometry x="-0.6109" relative="1" as="geometry">
<mxPoint x="20" y="2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-105" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-94" target="9zziB1Dtd-V9IfowJO3V-110" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-106" value="No" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-105" vertex="1" connectable="0">
<mxGeometry x="-0.1779" y="-2" relative="1" as="geometry">
<mxPoint y="-2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-94" value="Has Existing&lt;br&gt;Commission" style="rhombus;whiteSpace=wrap;html=1;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="310" y="830.52" width="100" height="100" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-101" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-96" target="9zziB1Dtd-V9IfowJO3V-99" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-96" value="View Existing&lt;br&gt;Commission Status" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="350" y="970" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-99" value="View All Commissions" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="500" y="770" width="130" height="27.48" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-109" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-104" target="9zziB1Dtd-V9IfowJO3V-96" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="560" y="985" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-104" value="Request New&lt;br&gt;Commission" style="whiteSpace=wrap;html=1;fillColor=#60a917;strokeColor=#2D7600;rounded=0;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="470" y="817.52" width="120" height="39.48" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-111" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=1;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-110" target="9zziB1Dtd-V9IfowJO3V-96" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="440" y="930" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-112" value="No" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-111" vertex="1" connectable="0">
<mxGeometry x="0.0044" relative="1" as="geometry">
<mxPoint y="5" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-113" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="9zziB1Dtd-V9IfowJO3V-110" target="9zziB1Dtd-V9IfowJO3V-104" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-114" value="Yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="9zziB1Dtd-V9IfowJO3V-113" vertex="1" connectable="0">
<mxGeometry x="-0.5043" y="1" relative="1" as="geometry">
<mxPoint x="3" y="-1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="9zziB1Dtd-V9IfowJO3V-110" value="Requests&lt;br&gt;Open" style="rhombus;whiteSpace=wrap;html=1;rounded=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="440" y="860.52" width="79.48" height="79.48" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-5" value="public.tags" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="207.5" y="440" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-6" value="name" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-5">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-9" value="public.admins" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="243" y="60" width="175" height="120" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-10" value="PK FK Unique uuid user_id" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-9">
<mxGeometry y="30" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-11" value="assigned_date:date" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-9">
<mxGeometry y="60" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-12" value="assigner_id:uuid?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-9">
<mxGeometry y="90" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-17" value="auth.users" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" vertex="1" parent="1">
<mxGeometry x="10" y="120" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-18" value="id" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-17">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#0050ef;strokeColor=#001DBC;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-18" target="1AZqCnQGpeGdDfHHl4o8-10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#0050ef;strokeColor=#001DBC;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-18" target="1AZqCnQGpeGdDfHHl4o8-14">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-23" value="public.tier" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#a20025;fontColor=#ffffff;strokeColor=#6F0000;" vertex="1" parent="1">
<mxGeometry x="32.5" y="290" width="140" height="120" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-24" value="PK name:text" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-23">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-25" value="price:decimal" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-23">
<mxGeometry y="60" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-26" value="stripe_product_id:string" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-23">
<mxGeometry y="90" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-29" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#a20025;strokeColor=#6F0000;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-24" target="1AZqCnQGpeGdDfHHl4o8-15">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-34" value="public.galleries" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="497.5" y="320" width="140" height="180" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-35" value="PK string name" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-34">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-37" value="FK tier string" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-34">
<mxGeometry y="60" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-38" value="string thumbnail_file" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-34">
<mxGeometry y="90" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-41" value="bool nsfw" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-34">
<mxGeometry y="120" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-39" value="text[] tags" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-34">
<mxGeometry y="150" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-42" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#a20025;strokeColor=#6F0000;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-24" target="1AZqCnQGpeGdDfHHl4o8-37">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="207.5" y="335" />
<mxPoint x="207.5" y="395" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-43" value="public.skibs" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="499.5" width="140" height="300" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-44" value="PK AI id:&amp;nbsp;int8" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="30" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-46" value="amount:decimal" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="60" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-49" value="user_id:uuid" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="90" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-50" value="request_date:date" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="120" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-51" value="accepted:bool?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="150" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-58" value="processed_date:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="180" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-52" value="payment_url:string" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="210" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-57" value="paidDate:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="240" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-59" value="completedDate:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-43">
<mxGeometry y="270" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-48" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;startArrow=classic;startFill=1;fillColor=#1ba1e2;strokeColor=#006EAF;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-6" target="1AZqCnQGpeGdDfHHl4o8-39">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-56" value="No FK Relationship" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontColor=#FFFFFF;labelBackgroundColor=none;" vertex="1" connectable="0" parent="1AZqCnQGpeGdDfHHl4o8-48">
<mxGeometry x="-0.0082" y="2" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-13" value="public.user_subscriptions" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="243" y="203" width="175" height="180" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-14" value="PK FK Unique user_id:uuid" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-13">
<mxGeometry y="30" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-15" value="FK tier:text" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-13">
<mxGeometry y="60" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-54" value="active:bool" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-13">
<mxGeometry y="90" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-53" value="last_paid_date:date" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-13">
<mxGeometry y="120" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-47" value="start_date:date" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-13">
<mxGeometry y="150" width="175" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-55" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#0050ef;strokeColor=#001DBC;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-18" target="1AZqCnQGpeGdDfHHl4o8-49">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="197.5" y="165" />
<mxPoint x="197.5" y="30" />
<mxPoint x="447.5" y="30" />
<mxPoint x="447.5" y="105" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-60" value="public.commissions" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="697.5" y="30" width="170" height="330" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-61" value="PK AI id:&amp;nbsp;int8" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="30" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-62" value="amount:decimal" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="60" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-63" value="user_id:uuid" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="90" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-64" value="request_date:date" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="120" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-65" value="accepted:bool?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="150" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-66" value="processed_date:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="180" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-67" value="hours:number" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="210" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-70" value="pending_approval:bool false" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="240" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-68" value="paidDate:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="270" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-69" value="completedDate:date?" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-60">
<mxGeometry y="300" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-71" value="public.comission_messages" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="957.5" y="180" width="210" height="120" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-72" value="PK AI id:int8" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-71">
<mxGeometry y="30" width="210" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-73" value="FK comission_id:int8" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-71">
<mxGeometry y="60" width="210" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-74" value="FK sender_id:uuid" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" vertex="1" parent="1AZqCnQGpeGdDfHHl4o8-71">
<mxGeometry y="90" width="210" height="30" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-79" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#0050ef;strokeColor=#001DBC;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-18" target="1AZqCnQGpeGdDfHHl4o8-63">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="197.5" y="165" />
<mxPoint x="197.5" y="30" />
<mxPoint x="447.5" y="30" />
<mxPoint x="447.5" y="-30" />
<mxPoint x="667.5" y="-30" />
<mxPoint x="667.5" y="135" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-80" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#1ba1e2;strokeColor=#006EAF;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-61" target="1AZqCnQGpeGdDfHHl4o8-73">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="1AZqCnQGpeGdDfHHl4o8-81" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#0050ef;strokeColor=#001DBC;" edge="1" parent="1" source="1AZqCnQGpeGdDfHHl4o8-18" target="1AZqCnQGpeGdDfHHl4o8-74">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="197.5" y="165" />
<mxPoint x="197.5" y="30" />
<mxPoint x="447.5" y="30" />
<mxPoint x="447.5" y="-30" />
<mxPoint x="667.5" y="-30" />
<mxPoint x="667.5" y="400" />
<mxPoint x="897.5" y="400" />
<mxPoint x="897.5" y="285" />
</Array>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

1089
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,8 @@
"@supabase/ssr": "latest", "@supabase/ssr": "latest",
"@supabase/supabase-js": "latest", "@supabase/supabase-js": "latest",
"@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/aspect-ratio": "^0.4.2",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.11",
"autoprefixer": "10.4.17", "autoprefixer": "10.4.17",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^11.2.6", "framer-motion": "^11.2.6",
@ -22,6 +24,9 @@
"react-easy-panzoom": "^0.4.4", "react-easy-panzoom": "^0.4.4",
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",
"react-responsive-masonry": "^2.2.0", "react-responsive-masonry": "^2.2.0",
"react-select": "^5.8.0",
"react-tailwindcss-select": "^1.8.5",
"sharp": "^0.33.4",
"simplex-noise": "^4.0.1", "simplex-noise": "^4.0.1",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.1",

4
supabase/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Supabase
.branches
.temp
.env

View File

@ -1,3 +0,0 @@
# Supabase Docker
This is a minimal Docker Compose setup for self-hosting Supabase. Follow the steps [here](https://supabase.com/docs/guides/hosting/docker) to get started.

171
supabase/config.toml Normal file
View File

@ -0,0 +1,171 @@
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "neroshitron"
[api]
enabled = true
# Port to use for the API URL.
port = 54321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` is always included.
schemas = ["public", "graphql_public"]
# Extra schemas to add to the search_path of every request. `public` is always included.
extra_search_path = ["public", "extensions"]
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
# for accidental or malicious requests.
max_rows = 1000
[db]
# Port to use for the local database URL.
port = 54322
# Port used by db diff command to initialize the shadow database.
shadow_port = 54320
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 15
[db.pooler]
enabled = false
# Port to use for the local connection pooler.
port = 54329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
# How many server connections to allow per user/database pair.
default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100
[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
# ip_version = "IPv6"
# The maximum length in bytes of HTTP request headers. (default: 4096)
# max_header_length = 4096
[studio]
enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
openai_api_key = "env(OPENAI_API_KEY)"
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
# are monitored, and you can view the emails that would have been sent from the web interface.
[inbucket]
enabled = true
# Port to use for the email testing server web interface.
port = 54324
# Uncomment to expose additional ports for testing user applications that send emails.
# smtp_port = 54325
# pop3_port = 54326
[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"
[storage.image_transformation]
enabled = true
[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.
enable_refresh_token_rotation = true
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
# Requires enable_refresh_token_rotation = true.
refresh_token_reuse_interval = 10
# Allow/disallow new user signups to your project.
enable_signup = true
# Allow/disallow anonymous sign-ins to your project.
enable_anonymous_sign_ins = false
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false
[auth.email]
# Allow/disallow new user signups via email to your project.
enable_signup = true
# If enabled, a user will be required to confirm any email change on both the old, and new email
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"
# Uncomment to customize email template
# [auth.email.template.invite]
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"
[auth.sms]
# Allow/disallow new user signups via SMS to your project.
enable_signup = true
# If enabled, users need to confirm their phone number before signing in.
enable_confirmations = false
# Template for sending OTP to users
template = "Your code is {{ .Code }} ."
# Controls the minimum amount of time that must pass before sending another sms otp.
max_frequency = "5s"
# Use pre-defined map of phone number to OTP for testing.
# [auth.sms.test_otp]
# 4152127777 = "123456"
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
# [auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
[auth.sms.twilio]
enabled = false
account_sid = ""
message_service_sid = ""
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
[auth.external.apple]
enabled = false
client_id = ""
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
# Overrides the default auth redirectUrl.
redirect_uri = ""
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false
[analytics]
enabled = false
port = 54327
vector_port = 54328
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"
# Experimental features may be deprecated any time
[experimental]
# Configures Postgres storage engine to use OrioleDB (S3)
orioledb_version = ""
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
s3_host = "env(S3_HOST)"
# Configures S3 bucket region, eg. us-east-1
s3_region = "env(S3_REGION)"
# Configures AWS_ACCESS_KEY_ID for S3 bucket
s3_access_key = "env(S3_ACCESS_KEY)"
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = "env(S3_SECRET_KEY)"

View File

@ -1,48 +0,0 @@
create table profiles (
id uuid references auth.users not null,
updated_at timestamp with time zone,
username text unique,
avatar_url text,
website text,
primary key (id),
unique(username),
constraint username_length check (char_length(username) >= 3)
);
alter table profiles enable row level security;
create policy "Public profiles are viewable by the owner."
on profiles for select
using ( auth.uid() = id );
create policy "Users can insert their own profile."
on profiles for insert
with check ( auth.uid() = id );
create policy "Users can update own profile."
on profiles for update
using ( auth.uid() = id );
-- Set up Realtime
begin;
drop publication if exists supabase_realtime;
create publication supabase_realtime;
commit;
alter publication supabase_realtime add table profiles;
-- Set up Storage
insert into storage.buckets (id, name)
values ('avatars', 'avatars');
create policy "Avatar images are publicly accessible."
on storage.objects for select
using ( bucket_id = 'avatars' );
create policy "Anyone can upload an avatar."
on storage.objects for insert
with check ( bucket_id = 'avatars' );
create policy "Anyone can update an avatar."
on storage.objects for update
with check ( bucket_id = 'avatars' );

View File

@ -1,34 +0,0 @@
version: "3.8"
services:
studio:
build:
context: ..
dockerfile: studio/Dockerfile
target: dev
ports:
- 8082:8082
mail:
container_name: supabase-mail
image: inbucket/inbucket:3.0.3
ports:
- '2500:2500' # SMTP
- '9000:9000' # web interface
- '1100:1100' # POP3
auth:
environment:
- GOTRUE_SMTP_USER=
- GOTRUE_SMTP_PASS=
meta:
ports:
- 5555:8080
db:
restart: 'no'
volumes:
# Always use a fresh database when developing
- /var/lib/postgresql/data
# Seed data should be inserted last (alphabetical order)
- ./dev/data.sql:/docker-entrypoint-initdb.d/seed.sql
storage:
volumes:
- /var/lib/storage

View File

@ -1,96 +0,0 @@
version: "3.8"
services:
minio:
image: minio/minio
ports:
- '9000:9000'
- '9001:9001'
environment:
MINIO_ROOT_USER: supa-storage
MINIO_ROOT_PASSWORD: secret1234
command: server --console-address ":9001" /data
healthcheck:
test: [ "CMD", "curl", "-f", "http://minio:9000/minio/health/live" ]
interval: 2s
timeout: 10s
retries: 5
volumes:
- ./volumes/storage:/data:z
minio-createbucket:
image: minio/mc
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
/usr/bin/mc alias set supa-minio http://minio:9000 supa-storage secret1234;
/usr/bin/mc mb supa-minio/stub;
exit 0;
"
storage:
container_name: supabase-storage
image: supabase/storage-api:v0.43.11
depends_on:
db:
# Disable this if you are using an external Postgres database
condition: service_healthy
rest:
condition: service_started
imgproxy:
condition: service_started
minio:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:5000/status"
]
timeout: 5s
interval: 5s
retries: 3
restart: unless-stopped
environment:
ANON_KEY: ${ANON_KEY}
SERVICE_KEY: ${SERVICE_ROLE_KEY}
POSTGREST_URL: http://rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET}
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
FILE_SIZE_LIMIT: 52428800
STORAGE_BACKEND: s3
GLOBAL_S3_BUCKET: stub
GLOBAL_S3_ENDPOINT: http://minio:9000
GLOBAL_S3_PROTOCOL: http
GLOBAL_S3_FORCE_PATH_STYLE: true
AWS_ACCESS_KEY_ID: supa-storage
AWS_SECRET_ACCESS_KEY: secret1234
AWS_DEFAULT_REGION: stub
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
# TODO: https://github.com/supabase/storage-api/issues/55
REGION: stub
ENABLE_IMAGE_TRANSFORMATION: "true"
IMGPROXY_URL: http://imgproxy:5001
volumes:
- ./volumes/storage:/var/lib/storage:z
imgproxy:
container_name: supabase-imgproxy
image: darthsim/imgproxy:v3.8.0
healthcheck:
test: [ "CMD", "imgproxy", "health" ]
timeout: 5s
interval: 5s
retries: 3
environment:
IMGPROXY_BIND: ":5001"
IMGPROXY_USE_ETAG: "true"
IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION}

View File

@ -1,430 +0,0 @@
# Usage
# Start: docker compose up
# With helpers: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up
# Stop: docker compose down
# Destroy: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans
name: supabase
version: "3.8"
services:
studio:
container_name: supabase-studio
image: supabase/studio:20240422-5cf8f30
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('http').get('http://localhost:3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
]
timeout: 5s
interval: 5s
retries: 3
depends_on:
analytics:
condition: service_healthy
environment:
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
SUPABASE_URL: http://kong:8000
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
LOGFLARE_API_KEY: ${LOGFLARE_API_KEY}
LOGFLARE_URL: http://analytics:4000
NEXT_PUBLIC_ENABLE_LOGS: true
# Comment to use Big Query backend for analytics
NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
# Uncomment to use Big Query backend for analytics
# NEXT_ANALYTICS_BACKEND_PROVIDER: bigquery
kong:
container_name: supabase-kong
image: kong:2.8.1
restart: unless-stopped
# https://unix.stackexchange.com/a/294837
entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
ports:
- ${KONG_HTTP_PORT}:8000/tcp
- ${KONG_HTTPS_PORT}:8443/tcp
depends_on:
analytics:
condition: service_healthy
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
# https://github.com/supabase/cli/issues/14
KONG_DNS_ORDER: LAST,A,CNAME
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
volumes:
# https://github.com/supabase/supabase/issues/12661
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro
auth:
container_name: supabase-auth
image: supabase/gotrue:v2.151.0
depends_on:
db:
# Disable this if you are using an external Postgres database
condition: service_healthy
analytics:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:9999/health"
]
timeout: 5s
interval: 5s
retries: 3
restart: unless-stopped
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
GOTRUE_SITE_URL: ${SITE_URL}
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
# GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true
# GOTRUE_SMTP_MAX_FREQUENCY: 1s
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL}
GOTRUE_SMTP_HOST: ${SMTP_HOST}
GOTRUE_SMTP_PORT: ${SMTP_PORT}
GOTRUE_SMTP_USER: ${SMTP_USER}
GOTRUE_SMTP_PASS: ${SMTP_PASS}
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE}
GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION}
GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY}
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE}
GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
# Uncomment to enable custom access token hook. You'll need to create a public.custom_access_token_hook function and grant necessary permissions.
# See: https://supabase.com/docs/guides/auth/auth-hooks#hook-custom-access-token for details
# GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED="true"
# GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI="pg-functions://postgres/public/custom_access_token_hook"
# GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED="true"
# GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI="pg-functions://postgres/public/mfa_verification_attempt"
# GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED="true"
# GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="pg-functions://postgres/public/password_verification_attempt"
rest:
container_name: supabase-rest
image: postgrest/postgrest:v12.0.1
depends_on:
db:
# Disable this if you are using an external Postgres database
condition: service_healthy
analytics:
condition: service_healthy
restart: unless-stopped
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
command: "postgrest"
realtime:
# This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
container_name: realtime-dev.supabase-realtime
image: supabase/realtime:v2.28.32
depends_on:
db:
# Disable this if you are using an external Postgres database
condition: service_healthy
analytics:
condition: service_healthy
healthcheck:
test:
[
"CMD",
"curl",
"-sSfL",
"--head",
"-o",
"/dev/null",
"-H",
"Authorization: Bearer ${ANON_KEY}",
"http://localhost:4000/api/tenants/realtime-dev/health"
]
timeout: 5s
interval: 5s
retries: 3
restart: unless-stopped
environment:
PORT: 4000
DB_HOST: ${POSTGRES_HOST}
DB_PORT: ${POSTGRES_PORT}
DB_USER: supabase_admin
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: ${POSTGRES_DB}
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
DB_ENC_KEY: supabaserealtime
API_JWT_SECRET: ${JWT_SECRET}
FLY_ALLOC_ID: fly123
FLY_APP_NAME: realtime
SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq
ERL_AFLAGS: -proto_dist inet_tcp
ENABLE_TAILSCALE: "false"
DNS_NODES: "''"
command: >
sh -c "/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server"
# To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up
storage:
container_name: supabase-storage
image: supabase/storage-api:v1.0.6
depends_on:
db:
# Disable this if you are using an external Postgres database
condition: service_healthy
rest:
condition: service_started
imgproxy:
condition: service_started
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:5000/status"
]
timeout: 5s
interval: 5s
retries: 3
restart: unless-stopped
environment:
ANON_KEY: ${ANON_KEY}
SERVICE_KEY: ${SERVICE_ROLE_KEY}
POSTGREST_URL: http://rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET}
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
FILE_SIZE_LIMIT: 52428800
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
# TODO: https://github.com/supabase/storage-api/issues/55
REGION: stub
GLOBAL_S3_BUCKET: stub
ENABLE_IMAGE_TRANSFORMATION: "true"
IMGPROXY_URL: http://imgproxy:5001
volumes:
- ./volumes/storage:/var/lib/storage:z
imgproxy:
container_name: supabase-imgproxy
image: darthsim/imgproxy:v3.8.0
healthcheck:
test: [ "CMD", "imgproxy", "health" ]
timeout: 5s
interval: 5s
retries: 3
environment:
IMGPROXY_BIND: ":5001"
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
IMGPROXY_USE_ETAG: "true"
IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION}
volumes:
- ./volumes/storage:/var/lib/storage:z
meta:
container_name: supabase-meta
image: supabase/postgres-meta:v0.80.0
depends_on:
db:
# Disable this if you are using an external Postgres database
condition: service_healthy
analytics:
condition: service_healthy
restart: unless-stopped
environment:
PG_META_PORT: 8080
PG_META_DB_HOST: ${POSTGRES_HOST}
PG_META_DB_PORT: ${POSTGRES_PORT}
PG_META_DB_NAME: ${POSTGRES_DB}
PG_META_DB_USER: supabase_admin
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
functions:
container_name: supabase-edge-functions
image: supabase/edge-runtime:v1.45.2
restart: unless-stopped
depends_on:
analytics:
condition: service_healthy
environment:
JWT_SECRET: ${JWT_SECRET}
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
# TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786
VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}"
volumes:
- ./volumes/functions:/home/deno/functions:Z
command:
- start
- --main-service
- /home/deno/functions/main
analytics:
container_name: supabase-analytics
image: supabase/logflare:1.4.0
healthcheck:
test: [ "CMD", "curl", "http://localhost:4000/health" ]
timeout: 5s
interval: 5s
retries: 10
restart: unless-stopped
depends_on:
db:
# Disable this if you are using an external Postgres database
condition: service_healthy
# Uncomment to use Big Query backend for analytics
# volumes:
# - type: bind
# source: ${PWD}/gcloud.json
# target: /opt/app/rel/logflare/bin/gcloud.json
# read_only: true
environment:
LOGFLARE_NODE_HOST: 127.0.0.1
DB_USERNAME: supabase_admin
DB_DATABASE: ${POSTGRES_DB}
DB_HOSTNAME: ${POSTGRES_HOST}
DB_PORT: ${POSTGRES_PORT}
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_SCHEMA: _analytics
LOGFLARE_API_KEY: ${LOGFLARE_API_KEY}
LOGFLARE_SINGLE_TENANT: true
LOGFLARE_SUPABASE_MODE: true
LOGFLARE_MIN_CLUSTER_SIZE: 1
# Comment variables to use Big Query backend for analytics
POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
POSTGRES_BACKEND_SCHEMA: _analytics
LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true
# Uncomment to use Big Query backend for analytics
# GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID}
# GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER}
ports:
- 4000:4000
# Comment out everything below this point if you are using an external Postgres database
db:
container_name: supabase-db
image: supabase/postgres:15.1.1.41
healthcheck:
test: pg_isready -U postgres -h localhost
interval: 5s
timeout: 5s
retries: 10
depends_on:
vector:
condition: service_healthy
command:
- postgres
- -c
- config_file=/etc/postgresql/postgresql.conf
- -c
- log_min_messages=fatal # prevents Realtime polling queries from appearing in logs
restart: unless-stopped
ports:
# Pass down internal port because it's set dynamically by other services
- ${POSTGRES_PORT}:${POSTGRES_PORT}
environment:
POSTGRES_HOST: /var/run/postgresql
PGPORT: ${POSTGRES_PORT}
POSTGRES_PORT: ${POSTGRES_PORT}
PGPASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGDATABASE: ${POSTGRES_DB}
POSTGRES_DB: ${POSTGRES_DB}
JWT_SECRET: ${JWT_SECRET}
JWT_EXP: ${JWT_EXPIRY}
volumes:
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
# Must be superuser to create event trigger
- ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
# Must be superuser to alter reserved role
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
# Initialize the database settings with JWT_SECRET and JWT_EXP
- ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
# PGDATA directory is persisted between restarts
- ./volumes/db/data:/var/lib/postgresql/data:Z
# Changes required for Analytics support
- ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z
# Use named volume to persist pgsodium decryption key between restarts
- db-config:/etc/postgresql-custom
vector:
container_name: supabase-vector
image: timberio/vector:0.28.1-alpine
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://vector:9001/health"
]
timeout: 5s
interval: 5s
retries: 3
volumes:
- ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro
- ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro
environment:
LOGFLARE_API_KEY: ${LOGFLARE_API_KEY}
command: [ "--config", "etc/vector/vector.yml" ]
volumes:
db-config:

View File

@ -0,0 +1,237 @@
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions";
CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium";
COMMENT ON SCHEMA "public" IS 'standard public schema';
CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";
CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";
CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";
CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
CREATE TYPE "public"."interface_config_option" AS ENUM (
'color',
'image'
);
ALTER TYPE "public"."interface_config_option" OWNER TO "postgres";
CREATE TYPE "public"."tier" AS ENUM (
'Free',
'Tier 1',
'Tier 2',
'Tier 3'
);
ALTER TYPE "public"."tier" OWNER TO "postgres";
SET default_tablespace = '';
SET default_table_access_method = "heap";
CREATE TABLE IF NOT EXISTS "public"."admins" (
"user_id" "uuid" NOT NULL,
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
"assigner" "uuid"
);
ALTER TABLE "public"."admins" OWNER TO "postgres";
CREATE TABLE IF NOT EXISTS "public"."galleries" (
"name" "text" NOT NULL,
"column_number" bigint,
"tier" "text" DEFAULT 'Free'::"text",
"nsfw" boolean,
"tags" "text"[],
"thumbnail_file" "text" DEFAULT ''::"text"
);
ALTER TABLE "public"."galleries" OWNER TO "postgres";
CREATE TABLE IF NOT EXISTS "public"."interface_configurations" (
"name" "text" NOT NULL,
"value" "text" NOT NULL,
"type" "public"."interface_config_option" DEFAULT 'color'::"public"."interface_config_option" NOT NULL
);
ALTER TABLE "public"."interface_configurations" OWNER TO "postgres";
CREATE TABLE IF NOT EXISTS "public"."tags" (
"name" "text" NOT NULL,
"gallery_name" "text"
);
ALTER TABLE "public"."tags" OWNER TO "postgres";
CREATE TABLE IF NOT EXISTS "public"."tiers" (
"name" "text" NOT NULL,
"price" double precision NOT NULL,
"color" "text" NOT NULL,
"description" "text" NOT NULL
);
ALTER TABLE "public"."tiers" OWNER TO "postgres";
CREATE TABLE IF NOT EXISTS "public"."user_subscriptions" (
"user_id" "uuid" NOT NULL,
"tier" "public"."tier"
);
ALTER TABLE "public"."user_subscriptions" OWNER TO "postgres";
ALTER TABLE ONLY "public"."admins"
ADD CONSTRAINT "admins_pkey" PRIMARY KEY ("user_id");
ALTER TABLE ONLY "public"."galleries"
ADD CONSTRAINT "galleries_pkey" PRIMARY KEY ("name");
ALTER TABLE ONLY "public"."interface_configurations"
ADD CONSTRAINT "interface_configurations_pkey" PRIMARY KEY ("name");
ALTER TABLE ONLY "public"."tags"
ADD CONSTRAINT "tags_pkey" PRIMARY KEY ("name");
ALTER TABLE ONLY "public"."tiers"
ADD CONSTRAINT "tiers_pkey" PRIMARY KEY ("name");
ALTER TABLE ONLY "public"."user_subscriptions"
ADD CONSTRAINT "user_subscriptions_pkey" PRIMARY KEY ("user_id");
ALTER TABLE ONLY "public"."admins"
ADD CONSTRAINT "admins_assigner_fkey" FOREIGN KEY ("assigner") REFERENCES "auth"."users"("id");
ALTER TABLE ONLY "public"."admins"
ADD CONSTRAINT "admins_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id");
ALTER TABLE ONLY "public"."tags"
ADD CONSTRAINT "tags_gallery_name_fkey" FOREIGN KEY ("gallery_name") REFERENCES "public"."galleries"("name");
ALTER TABLE ONLY "public"."user_subscriptions"
ADD CONSTRAINT "user_subscriptions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id");
CREATE POLICY "Enable delete for admins" ON "public"."tags" FOR DELETE TO "authenticated" USING ((EXISTS ( SELECT 1
FROM "public"."admins"
WHERE ("admins"."user_id" = "auth"."uid"()))));
CREATE POLICY "Enable delete for users based on admins" ON "public"."galleries" FOR DELETE TO "authenticated" USING ((EXISTS ( SELECT 1
FROM "public"."admins"
WHERE ("admins"."user_id" = "auth"."uid"()))));
CREATE POLICY "Enable insert for admins" ON "public"."tags" FOR INSERT TO "authenticated" WITH CHECK ((EXISTS ( SELECT 1
FROM "public"."admins"
WHERE ("admins"."user_id" = "auth"."uid"()))));
CREATE POLICY "Enable insert for users based admins" ON "public"."galleries" FOR INSERT WITH CHECK ((EXISTS ( SELECT 1
FROM "public"."admins"
WHERE ("admins"."user_id" = "auth"."uid"()))));
CREATE POLICY "Enable read access for all users" ON "public"."admins" FOR SELECT USING (true);
CREATE POLICY "Enable read access for all users" ON "public"."galleries" FOR SELECT USING (true);
CREATE POLICY "Enable read access for all users" ON "public"."interface_configurations" FOR SELECT USING (true);
CREATE POLICY "Enable read access for all users" ON "public"."tags" FOR SELECT USING (true);
CREATE POLICY "Enable read access for all users" ON "public"."tiers" FOR SELECT USING (true);
CREATE POLICY "Enable read for users based on user_id" ON "public"."user_subscriptions" FOR SELECT USING (true);
CREATE POLICY "admin insert" ON "public"."tiers" FOR INSERT WITH CHECK ((EXISTS ( SELECT 1
FROM "public"."admins"
WHERE ("admins"."user_id" = "auth"."uid"()))));
CREATE POLICY "admin update" ON "public"."tiers" FOR UPDATE USING ((EXISTS ( SELECT 1
FROM "public"."admins"
WHERE ("admins"."user_id" = "auth"."uid"()))));
ALTER TABLE "public"."admins" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "delete for admin" ON "public"."tiers" FOR DELETE USING ((EXISTS ( SELECT 1
FROM "public"."admins"
WHERE ("admins"."user_id" = "auth"."uid"()))));
ALTER TABLE "public"."galleries" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "public"."interface_configurations" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "public"."tags" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "public"."tiers" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "update for admin" ON "public"."galleries" FOR UPDATE USING ((EXISTS ( SELECT 1
FROM "public"."admins"
WHERE ("admins"."user_id" = "auth"."uid"()))));
CREATE POLICY "update theme admin" ON "public"."interface_configurations" FOR UPDATE USING ((EXISTS ( SELECT 1
FROM "public"."admins"
WHERE ("admins"."user_id" = "auth"."uid"()))));
ALTER TABLE "public"."user_subscriptions" ENABLE ROW LEVEL SECURITY;
ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres";
GRANT USAGE ON SCHEMA "public" TO "postgres";
GRANT USAGE ON SCHEMA "public" TO "anon";
GRANT USAGE ON SCHEMA "public" TO "authenticated";
GRANT USAGE ON SCHEMA "public" TO "service_role";
GRANT ALL ON TABLE "public"."admins" TO "anon";
GRANT ALL ON TABLE "public"."admins" TO "authenticated";
GRANT ALL ON TABLE "public"."admins" TO "service_role";
GRANT ALL ON TABLE "public"."galleries" TO "anon";
GRANT ALL ON TABLE "public"."galleries" TO "authenticated";
GRANT ALL ON TABLE "public"."galleries" TO "service_role";
GRANT ALL ON TABLE "public"."interface_configurations" TO "anon";
GRANT ALL ON TABLE "public"."interface_configurations" TO "authenticated";
GRANT ALL ON TABLE "public"."interface_configurations" TO "service_role";
GRANT ALL ON TABLE "public"."tags" TO "anon";
GRANT ALL ON TABLE "public"."tags" TO "authenticated";
GRANT ALL ON TABLE "public"."tags" TO "service_role";
GRANT ALL ON TABLE "public"."tiers" TO "anon";
GRANT ALL ON TABLE "public"."tiers" TO "authenticated";
GRANT ALL ON TABLE "public"."tiers" TO "service_role";
GRANT ALL ON TABLE "public"."user_subscriptions" TO "anon";
GRANT ALL ON TABLE "public"."user_subscriptions" TO "authenticated";
GRANT ALL ON TABLE "public"."user_subscriptions" TO "service_role";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role";
RESET ALL;

379
supabase/seed.sql Normal file
View File

@ -0,0 +1,379 @@
SET session_replication_role = replica;
--
-- PostgreSQL database dump
--
-- Dumped from database version 15.1 (Ubuntu 15.1-1.pgdg20.04+1)
-- Dumped by pg_dump version 15.6 (Ubuntu 15.6-1.pgdg20.04+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Data for Name: audit_log_entries; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
INSERT INTO "auth"."audit_log_entries" ("instance_id", "id", "payload", "created_at", "ip_address") VALUES
('00000000-0000-0000-0000-000000000000', 'd337a17f-a756-46eb-a0e8-8ef80fd0510d', '{"action":"user_signedup","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"team","traits":{"provider":"email"}}', '2024-05-27 14:10:29.638476+00', ''),
('00000000-0000-0000-0000-000000000000', 'd3e71df0-114a-4490-aeeb-6f92c45bad74', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-05-27 14:10:29.64088+00', ''),
('00000000-0000-0000-0000-000000000000', '1eab1cf3-5656-42c2-9e0b-796222de0c55', '{"action":"logout","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account"}', '2024-05-27 14:15:04.733941+00', ''),
('00000000-0000-0000-0000-000000000000', '410c5c63-cba5-442a-a6bb-157acc0bd370', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-05-27 14:34:43.53365+00', ''),
('00000000-0000-0000-0000-000000000000', '262365e2-093b-493c-abff-d760344338e3', '{"action":"logout","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account"}', '2024-05-27 15:32:39.855999+00', ''),
('00000000-0000-0000-0000-000000000000', 'b3b7c848-8d46-468f-8a9f-be56f842e8f8', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-05-27 15:32:48.609554+00', ''),
('00000000-0000-0000-0000-000000000000', '96af268f-e323-43ec-ac10-d33b94c47321', '{"action":"user_invited","actor_id":"00000000-0000-0000-0000-000000000000","actor_username":"service_role","actor_via_sso":false,"log_type":"team","traits":{"user_email":"damienostler1@gmail.com","user_id":"e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd"}}', '2024-05-27 15:44:27.171353+00', ''),
('00000000-0000-0000-0000-000000000000', '98b879f8-3ec0-4b14-9114-e9702693edd7', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-27 16:33:16.635885+00', ''),
('00000000-0000-0000-0000-000000000000', '05788b7b-5d97-4449-b036-0d94c021f149', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-27 16:33:16.636596+00', ''),
('00000000-0000-0000-0000-000000000000', 'ec47cc2d-ee91-4bda-93cc-80966a366550', '{"action":"logout","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account"}', '2024-05-27 17:16:40.455383+00', ''),
('00000000-0000-0000-0000-000000000000', 'dee2b80d-2cb6-49b0-9391-d52318eba3c6', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-05-27 17:18:49.038768+00', ''),
('00000000-0000-0000-0000-000000000000', '50134af5-eeca-4e0a-93c9-30c93395843d', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-27 18:17:16.199899+00', ''),
('00000000-0000-0000-0000-000000000000', 'e1b7a78c-c40e-4348-bd41-eadc94a93ebb', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-27 18:17:16.200645+00', ''),
('00000000-0000-0000-0000-000000000000', '0fffa887-134b-4d3b-bf70-24246ec38013', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-27 19:15:16.230466+00', ''),
('00000000-0000-0000-0000-000000000000', '83236dbe-037b-4a7a-a326-7ff3c9759bd7', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-27 19:15:16.231644+00', ''),
('00000000-0000-0000-0000-000000000000', '6f4bb32b-4690-4640-98ac-feb06384ef62', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-27 20:13:20.140291+00', ''),
('00000000-0000-0000-0000-000000000000', 'e6e8d515-e14c-48ff-8c4d-27cea84d33f3', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-27 20:13:20.140793+00', ''),
('00000000-0000-0000-0000-000000000000', 'e878d51d-2524-4796-9288-88bcf8d1982b', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-27 21:11:27.314657+00', ''),
('00000000-0000-0000-0000-000000000000', '0d3f4871-8215-4ae7-920b-c261ba7eaf11', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-27 21:11:27.315598+00', ''),
('00000000-0000-0000-0000-000000000000', 'f70081c9-673b-4e8e-84c6-574d4a6fc095', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-28 00:06:54.425581+00', ''),
('00000000-0000-0000-0000-000000000000', 'ff4e9892-902a-4e6f-9493-976acd87e5ad', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-28 00:06:54.426626+00', ''),
('00000000-0000-0000-0000-000000000000', '13891f74-fb79-45bd-a202-3c3846723eb3', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-28 01:11:09.34946+00', ''),
('00000000-0000-0000-0000-000000000000', '6125acd2-6bd2-4ae1-8b41-4b54b8097a64', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-05-28 01:11:09.350568+00', ''),
('00000000-0000-0000-0000-000000000000', '904b1ae0-83cb-4bce-a1b9-65f4215e0095', '{"action":"logout","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account"}', '2024-05-28 01:45:53.937368+00', ''),
('00000000-0000-0000-0000-000000000000', '16bba433-9af2-4328-b0d2-d59a9d072edb', '{"action":"user_signedup","actor_id":"e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd","actor_username":"damienostler1@gmail.com","actor_via_sso":false,"log_type":"team","traits":{"provider":"email"}}', '2024-05-28 02:01:35.369669+00', ''),
('00000000-0000-0000-0000-000000000000', 'a80de210-524f-41a2-b437-a0a68a5b0dc5', '{"action":"login","actor_id":"e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd","actor_username":"damienostler1@gmail.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-05-28 02:01:35.372417+00', ''),
('00000000-0000-0000-0000-000000000000', '56e9bcfc-28ea-4fbf-aae2-a6c611da2536', '{"action":"token_refreshed","actor_id":"e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd","actor_username":"damienostler1@gmail.com","actor_via_sso":false,"log_type":"token"}', '2024-05-28 03:03:11.47339+00', ''),
('00000000-0000-0000-0000-000000000000', 'f661df3e-4600-4254-a606-dab87b0bf241', '{"action":"token_revoked","actor_id":"e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd","actor_username":"damienostler1@gmail.com","actor_via_sso":false,"log_type":"token"}', '2024-05-28 03:03:11.474173+00', ''),
('00000000-0000-0000-0000-000000000000', 'eccb045f-3fa3-4cc4-92b5-2181ae4c684e', '{"action":"token_refreshed","actor_id":"e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd","actor_username":"damienostler1@gmail.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 01:54:12.341648+00', ''),
('00000000-0000-0000-0000-000000000000', 'bdca3f89-401a-40b5-b1bd-7b053f72f15a', '{"action":"token_revoked","actor_id":"e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd","actor_username":"damienostler1@gmail.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 01:54:12.342909+00', ''),
('00000000-0000-0000-0000-000000000000', 'a76ea115-0732-45e4-a05f-cbf9aec04dab', '{"action":"logout","actor_id":"e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd","actor_username":"damienostler1@gmail.com","actor_via_sso":false,"log_type":"account"}', '2024-06-01 01:54:44.965028+00', ''),
('00000000-0000-0000-0000-000000000000', '73e58d43-80dc-4c40-93b8-66debb2d79f2', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-06-01 01:54:50.928035+00', ''),
('00000000-0000-0000-0000-000000000000', 'a7b0b5b0-42d0-4970-a799-f918e320a460', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 02:52:51.499031+00', ''),
('00000000-0000-0000-0000-000000000000', 'dd70c686-fc1b-42da-8aa1-91a6d7fbcc14', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 02:52:51.50256+00', ''),
('00000000-0000-0000-0000-000000000000', '09161c6c-3f38-4405-b26e-67fe90bc0005', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 03:51:46.507091+00', ''),
('00000000-0000-0000-0000-000000000000', '7fd17052-1b4a-4178-98b8-3b16b9de930a', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 03:51:46.5081+00', ''),
('00000000-0000-0000-0000-000000000000', '727021f7-1385-43eb-9139-5c8d0984fb07', '{"action":"logout","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account"}', '2024-06-01 04:28:20.313782+00', ''),
('00000000-0000-0000-0000-000000000000', '7df2cdbc-1c84-4847-8142-90477524ee7a', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-06-01 04:32:18.045015+00', ''),
('00000000-0000-0000-0000-000000000000', 'abb0a2d8-d525-4612-a364-9ba3ea22eabf', '{"action":"logout","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account"}', '2024-06-01 04:32:21.789565+00', ''),
('00000000-0000-0000-0000-000000000000', '21e30d5b-7f03-459f-814d-43757cf3f8bf', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-06-01 04:33:07.034577+00', ''),
('00000000-0000-0000-0000-000000000000', '1d8fca22-6ef6-42eb-a1e9-659e702e0568', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 05:31:30.614051+00', ''),
('00000000-0000-0000-0000-000000000000', '3a9c811c-fad9-4739-9e2f-807a65f92ebc', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 05:31:30.614956+00', ''),
('00000000-0000-0000-0000-000000000000', '7620c8d4-9662-4c6b-b04f-8bcf5fc37cd2', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 07:21:44.28552+00', ''),
('00000000-0000-0000-0000-000000000000', '4cfcd7b6-8a95-4713-9409-317734ae7b38', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 07:21:44.286437+00', ''),
('00000000-0000-0000-0000-000000000000', '41cd74f2-8882-4e30-ae82-b743901cae3a', '{"action":"logout","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account"}', '2024-06-01 07:37:28.737433+00', ''),
('00000000-0000-0000-0000-000000000000', 'a490a17d-b47e-4820-923d-e7c8bc88efb5', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-06-01 07:37:35.302764+00', ''),
('00000000-0000-0000-0000-000000000000', '3c44829d-3ec7-4698-95c3-b53597422759', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 08:36:15.774+00', ''),
('00000000-0000-0000-0000-000000000000', '5b2c215f-bfd1-4b59-a782-eaac62d1c458', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 08:36:15.775505+00', ''),
('00000000-0000-0000-0000-000000000000', '0a5a278a-9fd1-4bcc-a041-66f1302ac7e6', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 09:34:29.572756+00', ''),
('00000000-0000-0000-0000-000000000000', '89b87acd-362b-4c51-8c3a-5a5e356ed8e2', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 09:34:29.573904+00', ''),
('00000000-0000-0000-0000-000000000000', '8c413fec-a827-4ede-a533-2409f855d2e0', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 10:35:29.39825+00', ''),
('00000000-0000-0000-0000-000000000000', '786f5ba3-670c-4799-8b5d-9e2536b75ca0', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 10:35:29.399482+00', ''),
('00000000-0000-0000-0000-000000000000', 'da188f51-e5c3-467c-b9c4-7e2ba8430981', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 20:10:45.655684+00', ''),
('00000000-0000-0000-0000-000000000000', 'c415ee23-3497-4445-9b18-b3698dd35e16', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 20:10:45.656475+00', ''),
('00000000-0000-0000-0000-000000000000', '4c5c4c36-a506-4c80-b467-68618dbf10db', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 22:05:34.985137+00', ''),
('00000000-0000-0000-0000-000000000000', '52eb3774-6624-4987-b357-fb07ff22e368', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-01 22:05:34.986175+00', ''),
('00000000-0000-0000-0000-000000000000', '4c493923-b97a-4ca7-ad59-801212c59692', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 03:42:48.403833+00', ''),
('00000000-0000-0000-0000-000000000000', '04114bf3-53c5-447b-bb9d-0cf0e97411ce', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 03:42:48.405516+00', ''),
('00000000-0000-0000-0000-000000000000', '59785eb8-3a22-4596-b534-c76d0aaadf9b', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 04:41:14.398806+00', ''),
('00000000-0000-0000-0000-000000000000', '1da856fa-1582-447b-a033-6dfd6e91ee5b', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 04:41:14.399986+00', ''),
('00000000-0000-0000-0000-000000000000', '3790f005-05d8-439d-a828-fb8e6a22356f', '{"action":"logout","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account"}', '2024-06-02 05:20:51.896435+00', ''),
('00000000-0000-0000-0000-000000000000', '903ece4b-635c-4fd0-9a6d-c60103328e11', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-06-02 05:21:03.760977+00', ''),
('00000000-0000-0000-0000-000000000000', 'ab8d42bd-c9df-4782-8cbd-14279592fe94', '{"action":"logout","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account"}', '2024-06-02 06:18:43.051255+00', ''),
('00000000-0000-0000-0000-000000000000', '008ae3d1-61c4-4456-8e70-8a2030acbbc4', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-06-02 07:19:23.344522+00', ''),
('00000000-0000-0000-0000-000000000000', 'bf042910-d447-4f27-bb63-fbd6b80c266f', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 08:17:36.070061+00', ''),
('00000000-0000-0000-0000-000000000000', '55f64c6a-d6bd-4dd4-8627-3c35caa6dc7e', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 08:17:36.071183+00', ''),
('00000000-0000-0000-0000-000000000000', '578dc04e-5665-410c-8c0e-bbf40b397b18', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 09:23:17.276185+00', ''),
('00000000-0000-0000-0000-000000000000', '4ccf2be5-8ecd-48ee-9400-7fef1004dc56', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 09:23:17.277357+00', ''),
('00000000-0000-0000-0000-000000000000', '824ccdf1-4dc0-4cee-8c01-ac66cdea101f', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 10:21:37.85499+00', ''),
('00000000-0000-0000-0000-000000000000', '98aa3855-f2ff-4118-9618-041c82318963', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 10:21:37.856028+00', ''),
('00000000-0000-0000-0000-000000000000', 'f1599f7e-e1c1-4861-87e5-9b8e5e9fa8f7', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 11:20:00.113655+00', ''),
('00000000-0000-0000-0000-000000000000', '84f7562d-151d-4e30-b810-70891f3a8a2b', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 11:20:00.115274+00', ''),
('00000000-0000-0000-0000-000000000000', '7451f114-d011-4552-9b21-bcf6d6df6243', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 20:49:23.877784+00', ''),
('00000000-0000-0000-0000-000000000000', '24b82857-e2fd-4732-a2e8-ac99f67fffa9', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 20:49:23.878598+00', ''),
('00000000-0000-0000-0000-000000000000', 'bf2935a0-244a-449d-925c-ca7ad6bf91d6', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 21:49:06.535623+00', ''),
('00000000-0000-0000-0000-000000000000', '88b130fb-5418-49ec-9e54-7db1df9b9353', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 21:49:06.536554+00', ''),
('00000000-0000-0000-0000-000000000000', '2aafc337-642d-4f4c-b550-b1c88b0f698a', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 22:47:33.949246+00', ''),
('00000000-0000-0000-0000-000000000000', '7b7bc3c1-3751-4e00-a08c-14b5bb3cf892', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-02 22:47:33.950537+00', ''),
('00000000-0000-0000-0000-000000000000', '7b3b5a37-ed60-4b75-84ae-0caba9254581', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-06-03 00:10:51.713229+00', ''),
('00000000-0000-0000-0000-000000000000', 'd984ff2d-6d96-43d9-9a39-703de7400fd1', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 01:10:47.663131+00', ''),
('00000000-0000-0000-0000-000000000000', 'e996a172-f818-489a-84b3-8dc8b8a1068b', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 01:10:47.663668+00', ''),
('00000000-0000-0000-0000-000000000000', '0f07359e-4c78-40a7-8711-4127dc74c0fb', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 02:08:53.524332+00', ''),
('00000000-0000-0000-0000-000000000000', 'b8f1d6e8-4e3f-473c-b0be-98f4540da6f3', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 02:08:53.525533+00', ''),
('00000000-0000-0000-0000-000000000000', '1043d8ee-0301-46e0-a451-2bb043fe6634', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 03:07:09.462821+00', ''),
('00000000-0000-0000-0000-000000000000', '6924eb3a-0588-46ff-94dc-b45f29a06e74', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 03:07:09.463674+00', ''),
('00000000-0000-0000-0000-000000000000', '3995cc04-3976-47bd-aafb-04fcf8a533db', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 04:05:10.557103+00', ''),
('00000000-0000-0000-0000-000000000000', 'd5cb3c9b-aa2d-4ca6-9067-4c31ecbb7827', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 04:05:10.558474+00', ''),
('00000000-0000-0000-0000-000000000000', '8c7ca4b6-0b87-4704-9508-8cdfaac13c65', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 05:04:07.678559+00', ''),
('00000000-0000-0000-0000-000000000000', '7a1eb770-9f3c-4ac2-a13a-fafe80704bef', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 05:04:07.679535+00', ''),
('00000000-0000-0000-0000-000000000000', 'c4c2ae8b-62eb-4dde-8023-bb4a52fd8402', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 06:02:18.158106+00', ''),
('00000000-0000-0000-0000-000000000000', 'f3deb712-a27b-4652-b3dc-bb782188943c', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-03 06:02:18.159666+00', ''),
('00000000-0000-0000-0000-000000000000', 'db502a30-3a84-4151-8c02-7be7eba0f8d1', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-04 00:29:24.732764+00', ''),
('00000000-0000-0000-0000-000000000000', 'a24f4f60-00aa-48eb-8318-ed131fd44b2e', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-04 00:29:24.734165+00', ''),
('00000000-0000-0000-0000-000000000000', 'f1d4e360-3355-4850-9f9e-64961bb1cadf', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-04 01:27:58.415574+00', ''),
('00000000-0000-0000-0000-000000000000', '45e4135f-e65c-4011-bd07-c6a5bcb33860', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-04 01:27:58.416715+00', ''),
('00000000-0000-0000-0000-000000000000', '95c4b03d-1f15-4ad9-83c1-79f079407908', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-04 02:26:28.440011+00', ''),
('00000000-0000-0000-0000-000000000000', '9b5db737-976a-459f-8f12-eed61184218d', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-04 02:26:28.441438+00', ''),
('00000000-0000-0000-0000-000000000000', '2bbe5699-d14f-4c3f-9c87-52b7f01ed087', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-05 00:52:31.052995+00', ''),
('00000000-0000-0000-0000-000000000000', 'b54ff1b9-6254-43d2-a210-b5d9d2701974', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-05 00:52:31.055077+00', ''),
('00000000-0000-0000-0000-000000000000', 'b0611a51-8bb8-4fab-937e-68a413e02b5e', '{"action":"logout","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account"}', '2024-06-05 01:03:30.844197+00', ''),
('00000000-0000-0000-0000-000000000000', '57f72aa6-2609-4efe-bf06-950681c975f9', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-06-05 01:03:39.551264+00', ''),
('00000000-0000-0000-0000-000000000000', '3e6b6d3e-c8bd-4cd6-a7ac-b55c59269df1', '{"action":"login","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"account","traits":{"provider":"email"}}', '2024-06-05 01:06:08.194567+00', ''),
('00000000-0000-0000-0000-000000000000', '59805a05-bfba-4dbe-8021-8db6d6b71a4e', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-05 02:02:22.65237+00', ''),
('00000000-0000-0000-0000-000000000000', 'eaf050ba-9bdc-4771-8dcb-1534c38d68b9', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-05 02:02:22.653268+00', ''),
('00000000-0000-0000-0000-000000000000', 'a326a1f9-92f7-472c-bbb7-f10c6f3c1dff', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-05 03:00:26.781265+00', ''),
('00000000-0000-0000-0000-000000000000', '473362fc-dc06-43d5-99df-065c9e671347', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-05 03:00:26.782565+00', ''),
('00000000-0000-0000-0000-000000000000', '3a82cdde-76bf-4cc4-a5e2-b818601100a1', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-07 23:35:28.284863+00', ''),
('00000000-0000-0000-0000-000000000000', 'c3ecf585-5984-4b7e-8115-f57523a31e23', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-07 23:35:28.285858+00', ''),
('00000000-0000-0000-0000-000000000000', 'cc3425df-b1e8-4e7b-8729-80e66eae2b27', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 00:33:56.442635+00', ''),
('00000000-0000-0000-0000-000000000000', 'd991bd91-d600-457c-a887-67a3be188c83', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 00:33:56.44418+00', ''),
('00000000-0000-0000-0000-000000000000', 'a8bd8737-8115-4643-b9a7-f288be170b28', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 02:07:59.069381+00', ''),
('00000000-0000-0000-0000-000000000000', '495056f7-149e-43b3-bc58-1712e3787215', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 02:07:59.070256+00', ''),
('00000000-0000-0000-0000-000000000000', '27ca0341-280d-474a-a3da-39fa4c69748e', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 03:06:06.149018+00', ''),
('00000000-0000-0000-0000-000000000000', 'aa06c3b0-cd66-440a-a184-d44795552f11', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 03:06:06.15067+00', ''),
('00000000-0000-0000-0000-000000000000', '398046e7-2dc1-49e0-92ad-4823c0001271', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 04:04:06.664163+00', ''),
('00000000-0000-0000-0000-000000000000', 'eb481c2d-2f96-4efa-84cb-e715ee547913', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 04:04:06.665465+00', ''),
('00000000-0000-0000-0000-000000000000', '088f4537-0025-44e9-a499-dc85d7034ed3', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 22:08:31.582227+00', ''),
('00000000-0000-0000-0000-000000000000', '7f476998-602e-4d2e-9129-92ea021b1d50', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 22:08:31.583859+00', ''),
('00000000-0000-0000-0000-000000000000', '1c9bf13b-7ecf-4432-a83e-8dcd95eeb6c9', '{"action":"token_refreshed","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 23:06:38.987497+00', ''),
('00000000-0000-0000-0000-000000000000', '07c85a08-4a4b-4238-ac4c-047350db4b7f', '{"action":"token_revoked","actor_id":"893c7701-d5df-4415-80bd-1ec089764400","actor_username":"damienostler1@outlook.com","actor_via_sso":false,"log_type":"token"}', '2024-06-08 23:06:38.988275+00', '');
--
-- Data for Name: flow_state; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: users; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
INSERT INTO "auth"."users" ("instance_id", "id", "aud", "role", "email", "encrypted_password", "email_confirmed_at", "invited_at", "confirmation_token", "confirmation_sent_at", "recovery_token", "recovery_sent_at", "email_change_token_new", "email_change", "email_change_sent_at", "last_sign_in_at", "raw_app_meta_data", "raw_user_meta_data", "is_super_admin", "created_at", "updated_at", "phone", "phone_confirmed_at", "phone_change", "phone_change_token", "phone_change_sent_at", "email_change_token_current", "email_change_confirm_status", "banned_until", "reauthentication_token", "reauthentication_sent_at", "is_sso_user", "deleted_at", "is_anonymous") VALUES
('00000000-0000-0000-0000-000000000000', 'e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd', 'authenticated', 'authenticated', 'damienostler1@gmail.com', '', '2024-05-28 02:01:35.370491+00', '2024-05-27 15:44:27.171961+00', '', '2024-05-27 15:44:27.171961+00', '', NULL, '', '', NULL, '2024-05-28 02:01:35.372861+00', '{"provider": "email", "providers": ["email"]}', '{}', NULL, '2024-05-27 15:44:27.167772+00', '2024-06-01 01:54:12.344577+00', NULL, NULL, '', '', NULL, '', 0, NULL, '', NULL, false, NULL, false),
('00000000-0000-0000-0000-000000000000', '893c7701-d5df-4415-80bd-1ec089764400', 'authenticated', 'authenticated', 'damienostler1@outlook.com', '$2a$10$ISYdoWsKL7gxfRz7c5IKDOTsmcjNpGgg9OOApYLMOvtOoNTo4HGM6', '2024-05-27 14:10:29.639017+00', NULL, '', NULL, '', NULL, '', '', NULL, '2024-06-05 01:06:08.194961+00', '{"provider": "email", "providers": ["email"]}', '{"sub": "893c7701-d5df-4415-80bd-1ec089764400", "email": "damienostler1@outlook.com", "email_verified": false, "phone_verified": false}', NULL, '2024-05-27 14:10:29.634157+00', '2024-06-08 23:06:38.990134+00', NULL, NULL, '', '', NULL, '', 0, NULL, '', NULL, false, NULL, false);
--
-- Data for Name: identities; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
INSERT INTO "auth"."identities" ("provider_id", "user_id", "identity_data", "provider", "last_sign_in_at", "created_at", "updated_at", "id") VALUES
('893c7701-d5df-4415-80bd-1ec089764400', '893c7701-d5df-4415-80bd-1ec089764400', '{"sub": "893c7701-d5df-4415-80bd-1ec089764400", "email": "damienostler1@outlook.com", "email_verified": false, "phone_verified": false}', 'email', '2024-05-27 14:10:29.636992+00', '2024-05-27 14:10:29.637013+00', '2024-05-27 14:10:29.637013+00', 'b823bde7-9eae-4e1f-8253-75f12f0f06f2'),
('e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd', 'e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd', '{"sub": "e11e6fda-d1fc-4b0a-bd61-80233fcb5cdd", "email": "damienostler1@gmail.com", "email_verified": false, "phone_verified": false}', 'email', '2024-05-27 15:44:27.170495+00', '2024-05-27 15:44:27.170543+00', '2024-05-27 15:44:27.170543+00', 'f88a9c9a-1566-4c04-8340-3d233dcca864');
--
-- Data for Name: instances; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: sessions; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
INSERT INTO "auth"."sessions" ("id", "user_id", "created_at", "updated_at", "factor_id", "aal", "not_after", "refreshed_at", "user_agent", "ip", "tag") VALUES
('88176780-1709-4810-92de-36f909e3fafb', '893c7701-d5df-4415-80bd-1ec089764400', '2024-06-05 01:06:08.19499+00', '2024-06-05 01:06:08.19499+00', NULL, 'aal1', NULL, NULL, 'node', '192.168.65.1', NULL),
('8de4dae9-a9f9-44ad-bf2a-c42356feb0e2', '893c7701-d5df-4415-80bd-1ec089764400', '2024-06-05 01:03:39.551694+00', '2024-06-08 23:06:38.991049+00', NULL, 'aal1', NULL, '2024-06-08 23:06:38.990997', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', '192.168.65.1', NULL);
--
-- Data for Name: mfa_amr_claims; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
INSERT INTO "auth"."mfa_amr_claims" ("session_id", "created_at", "updated_at", "authentication_method", "id") VALUES
('8de4dae9-a9f9-44ad-bf2a-c42356feb0e2', '2024-06-05 01:03:39.553156+00', '2024-06-05 01:03:39.553156+00', 'password', '70b28b1d-9c1c-42ed-b9b6-4629b4dc6610'),
('88176780-1709-4810-92de-36f909e3fafb', '2024-06-05 01:06:08.195749+00', '2024-06-05 01:06:08.195749+00', 'password', '2d748036-15f9-49a1-ba18-a9636803f795');
--
-- Data for Name: mfa_factors; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: mfa_challenges; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: one_time_tokens; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: refresh_tokens; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
INSERT INTO "auth"."refresh_tokens" ("instance_id", "id", "token", "user_id", "revoked", "created_at", "updated_at", "parent", "session_id") VALUES
('00000000-0000-0000-0000-000000000000', 83, 'c6bYIcVnSBosWtn4QX8F6w', '893c7701-d5df-4415-80bd-1ec089764400', false, '2024-06-05 01:06:08.195296+00', '2024-06-05 01:06:08.195296+00', NULL, '88176780-1709-4810-92de-36f909e3fafb'),
('00000000-0000-0000-0000-000000000000', 82, 'vgibpWzclaC1SiIWnkWFTw', '893c7701-d5df-4415-80bd-1ec089764400', true, '2024-06-05 01:03:39.55241+00', '2024-06-05 02:02:22.653721+00', NULL, '8de4dae9-a9f9-44ad-bf2a-c42356feb0e2'),
('00000000-0000-0000-0000-000000000000', 84, 'kcsINvcmhD18guzedNei0Q', '893c7701-d5df-4415-80bd-1ec089764400', true, '2024-06-05 02:02:22.654159+00', '2024-06-05 03:00:26.783216+00', 'vgibpWzclaC1SiIWnkWFTw', '8de4dae9-a9f9-44ad-bf2a-c42356feb0e2'),
('00000000-0000-0000-0000-000000000000', 85, '4eJVeDMlvmAUk20ZqwdGQA', '893c7701-d5df-4415-80bd-1ec089764400', true, '2024-06-05 03:00:26.784329+00', '2024-06-07 23:35:28.286039+00', 'kcsINvcmhD18guzedNei0Q', '8de4dae9-a9f9-44ad-bf2a-c42356feb0e2'),
('00000000-0000-0000-0000-000000000000', 86, 'Nb0zuvJjDL8keK1lF1xX9Q', '893c7701-d5df-4415-80bd-1ec089764400', true, '2024-06-07 23:35:28.28691+00', '2024-06-08 00:33:56.445064+00', '4eJVeDMlvmAUk20ZqwdGQA', '8de4dae9-a9f9-44ad-bf2a-c42356feb0e2'),
('00000000-0000-0000-0000-000000000000', 119, 'y9oA8pEThLtPyEJW2IK3eA', '893c7701-d5df-4415-80bd-1ec089764400', true, '2024-06-08 00:33:56.446127+00', '2024-06-08 02:07:59.070607+00', 'Nb0zuvJjDL8keK1lF1xX9Q', '8de4dae9-a9f9-44ad-bf2a-c42356feb0e2'),
('00000000-0000-0000-0000-000000000000', 120, 'TwWGx47OPQs1BNtkLs9JSA', '893c7701-d5df-4415-80bd-1ec089764400', true, '2024-06-08 02:07:59.07107+00', '2024-06-08 03:06:06.151386+00', 'y9oA8pEThLtPyEJW2IK3eA', '8de4dae9-a9f9-44ad-bf2a-c42356feb0e2'),
('00000000-0000-0000-0000-000000000000', 121, 'mL_eZQCxVdYq7SR-UjEJgQ', '893c7701-d5df-4415-80bd-1ec089764400', true, '2024-06-08 03:06:06.154686+00', '2024-06-08 04:04:06.666124+00', 'TwWGx47OPQs1BNtkLs9JSA', '8de4dae9-a9f9-44ad-bf2a-c42356feb0e2'),
('00000000-0000-0000-0000-000000000000', 122, 'cFLCDOB1QfKd_ydDNLRmoQ', '893c7701-d5df-4415-80bd-1ec089764400', true, '2024-06-08 04:04:06.667264+00', '2024-06-08 22:08:31.584256+00', 'mL_eZQCxVdYq7SR-UjEJgQ', '8de4dae9-a9f9-44ad-bf2a-c42356feb0e2'),
('00000000-0000-0000-0000-000000000000', 123, 'G5KZsQQhC6kFqd0Xht2ypg', '893c7701-d5df-4415-80bd-1ec089764400', true, '2024-06-08 22:08:31.584969+00', '2024-06-08 23:06:38.988706+00', 'cFLCDOB1QfKd_ydDNLRmoQ', '8de4dae9-a9f9-44ad-bf2a-c42356feb0e2'),
('00000000-0000-0000-0000-000000000000', 124, '4p4Q-c4t1ZH2PUvl_mcYtQ', '893c7701-d5df-4415-80bd-1ec089764400', false, '2024-06-08 23:06:38.989251+00', '2024-06-08 23:06:38.989251+00', 'G5KZsQQhC6kFqd0Xht2ypg', '8de4dae9-a9f9-44ad-bf2a-c42356feb0e2');
--
-- Data for Name: sso_providers; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: saml_providers; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: saml_relay_states; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: sso_domains; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin
--
--
-- Data for Name: key; Type: TABLE DATA; Schema: pgsodium; Owner: supabase_admin
--
--
-- Data for Name: admins; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO "public"."admins" ("user_id", "created_at", "assigner") VALUES
('893c7701-d5df-4415-80bd-1ec089764400', '2024-06-02 21:12:58.507093+00', NULL);
--
-- Data for Name: galleries; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO "public"."galleries" ("name", "column_number", "tier", "nsfw", "tags", "thumbnail_file") VALUES
('Test', 3, 'Free', false, '{twatwat}', ''),
('Testsdadas', 3, 'Free', false, '{}', '');
--
-- Data for Name: interface_configurations; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO "public"."interface_configurations" ("name", "value", "type") VALUES
('primary-dark', '#100120', 'color'),
('secondary-light', '#6F5D90', 'color'),
('secondary-dark', '#2F1D50', 'color'),
('primary', '#18161d', 'color'),
('secondary', '#4F3D70', 'color'),
('primary-light', '#403260', 'color'),
('error-dark', '#5C0D0D', 'color'),
('error', '#862117', 'color'),
('success-dark', '#00A986', 'color'),
('error-light', '#C44C4C', 'color'),
('success-light', '#20E9C6', 'color'),
('success', '#00C9A6', 'color'),
('warning', '#E17558', 'color'),
('warning-light', '#E39578', 'color'),
('info-light', '#424260', 'color'),
('warning-dark', '#C15538', 'color'),
('info-dark', '#020120', 'color'),
('info', '#222140', 'color');
--
-- Data for Name: tags; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO "public"."tags" ("name", "gallery_name") VALUES
('twatwat', NULL),
('', NULL);
--
-- Data for Name: tiers; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO "public"."tiers" ("name", "price", "color", "description") VALUES
('Free', 0, '#8f6eaa', 'This is a test tier.dasdasd');
--
-- Data for Name: user_subscriptions; Type: TABLE DATA; Schema: public; Owner: postgres
--
INSERT INTO "public"."user_subscriptions" ("user_id", "tier") VALUES
('893c7701-d5df-4415-80bd-1ec089764400', 'Tier 2');
--
-- Data for Name: objects; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
--
--
-- Data for Name: s3_multipart_uploads; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
--
--
-- Data for Name: s3_multipart_uploads_parts; Type: TABLE DATA; Schema: storage; Owner: supabase_storage_admin
--
--
-- Data for Name: hooks; Type: TABLE DATA; Schema: supabase_functions; Owner: supabase_functions_admin
--
--
-- Data for Name: secrets; Type: TABLE DATA; Schema: vault; Owner: supabase_admin
--
--
-- Name: refresh_tokens_id_seq; Type: SEQUENCE SET; Schema: auth; Owner: supabase_auth_admin
--
SELECT pg_catalog.setval('"auth"."refresh_tokens_id_seq"', 124, true);
--
-- Name: key_key_id_seq; Type: SEQUENCE SET; Schema: pgsodium; Owner: supabase_admin
--
SELECT pg_catalog.setval('"pgsodium"."key_key_id_seq"', 1, false);
--
-- PostgreSQL database dump complete
--
RESET ALL;

View File

@ -1,241 +0,0 @@
_format_version: '2.1'
_transform: true
###
### Consumers / Users
###
consumers:
- username: DASHBOARD
- username: anon
keyauth_credentials:
- key: $SUPABASE_ANON_KEY
- username: service_role
keyauth_credentials:
- key: $SUPABASE_SERVICE_KEY
###
### Access Control List
###
acls:
- consumer: anon
group: anon
- consumer: service_role
group: admin
###
### Dashboard credentials
###
basicauth_credentials:
- consumer: DASHBOARD
username: $DASHBOARD_USERNAME
password: $DASHBOARD_PASSWORD
###
### API Routes
###
services:
## Open Auth routes
- name: auth-v1-open
url: http://auth:9999/verify
routes:
- name: auth-v1-open
strip_path: true
paths:
- /auth/v1/verify
plugins:
- name: cors
- name: auth-v1-open-callback
url: http://auth:9999/callback
routes:
- name: auth-v1-open-callback
strip_path: true
paths:
- /auth/v1/callback
plugins:
- name: cors
- name: auth-v1-open-authorize
url: http://auth:9999/authorize
routes:
- name: auth-v1-open-authorize
strip_path: true
paths:
- /auth/v1/authorize
plugins:
- name: cors
## Secure Auth routes
- name: auth-v1
_comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*'
url: http://auth:9999/
routes:
- name: auth-v1-all
strip_path: true
paths:
- /auth/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## Secure REST routes
- name: rest-v1
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
url: http://rest:3000/
routes:
- name: rest-v1-all
strip_path: true
paths:
- /rest/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: true
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## Secure GraphQL routes
- name: graphql-v1
_comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'
url: http://rest:3000/rpc/graphql
routes:
- name: graphql-v1-all
strip_path: true
paths:
- /graphql/v1
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: true
- name: request-transformer
config:
add:
headers:
- Content-Profile:graphql_public
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## Secure Realtime routes
- name: realtime-v1-ws
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
url: http://realtime-dev.supabase-realtime:4000/socket
protocol: ws
routes:
- name: realtime-v1-ws
strip_path: true
paths:
- /realtime/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
- name: realtime-v1-rest
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
url: http://realtime-dev.supabase-realtime:4000/api
protocol: http
routes:
- name: realtime-v1-rest
strip_path: true
paths:
- /realtime/v1/api
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## Storage routes: the storage server manages its own auth
- name: storage-v1
_comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
url: http://storage:5000/
routes:
- name: storage-v1-all
strip_path: true
paths:
- /storage/v1/
plugins:
- name: cors
## Edge Functions routes
- name: functions-v1
_comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'
url: http://functions:9000/
routes:
- name: functions-v1-all
strip_path: true
paths:
- /functions/v1/
plugins:
- name: cors
## Analytics routes
- name: analytics-v1
_comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'
url: http://analytics:4000/
routes:
- name: analytics-v1-all
strip_path: true
paths:
- /analytics/v1/
## Secure Database routes
- name: meta
_comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'
url: http://meta:8080/
routes:
- name: meta-all
strip_path: true
paths:
- /pg/
plugins:
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
## Protected Dashboard - catch all remaining routes
- name: dashboard
_comment: 'Studio: /* -> http://studio:3000/*'
url: http://studio:3000/
routes:
- name: dashboard-all
strip_path: true
paths:
- /
plugins:
- name: cors
- name: basic-auth
config:
hide_credentials: true

View File

@ -1,5 +0,0 @@
\set jwt_secret `echo "$JWT_SECRET"`
\set jwt_exp `echo "$JWT_EXP"`
ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret';
ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp';

View File

@ -1,4 +0,0 @@
\set pguser `echo "$POSTGRES_USER"`
create schema if not exists _analytics;
alter schema _analytics owner to :pguser;

View File

@ -1,4 +0,0 @@
\set pguser `echo "$POSTGRES_USER"`
create schema if not exists _realtime;
alter schema _realtime owner to :pguser;

View File

@ -1,8 +0,0 @@
-- NOTE: change to your own passwords for production environments
\set pgpass `echo "$POSTGRES_PASSWORD"`
ALTER USER authenticator WITH PASSWORD :'pgpass';
ALTER USER pgbouncer WITH PASSWORD :'pgpass';
ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';
ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass';
ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';

View File

@ -1,208 +0,0 @@
BEGIN;
-- Create pg_net extension
CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
-- Create supabase_functions schema
CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;
GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
-- supabase_functions.migrations definition
CREATE TABLE supabase_functions.migrations (
version text PRIMARY KEY,
inserted_at timestamptz NOT NULL DEFAULT NOW()
);
-- Initial supabase_functions migration
INSERT INTO supabase_functions.migrations (version) VALUES ('initial');
-- supabase_functions.hooks definition
CREATE TABLE supabase_functions.hooks (
id bigserial PRIMARY KEY,
hook_table_id integer NOT NULL,
hook_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT NOW(),
request_id bigint
);
CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);
CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);
COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
CREATE FUNCTION supabase_functions.http_request()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
request_id bigint;
payload jsonb;
url text := TG_ARGV[0]::text;
method text := TG_ARGV[1]::text;
headers jsonb DEFAULT '{}'::jsonb;
params jsonb DEFAULT '{}'::jsonb;
timeout_ms integer DEFAULT 1000;
BEGIN
IF url IS NULL OR url = 'null' THEN
RAISE EXCEPTION 'url argument is missing';
END IF;
IF method IS NULL OR method = 'null' THEN
RAISE EXCEPTION 'method argument is missing';
END IF;
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
headers = '{"Content-Type": "application/json"}'::jsonb;
ELSE
headers = TG_ARGV[2]::jsonb;
END IF;
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
params = '{}'::jsonb;
ELSE
params = TG_ARGV[3]::jsonb;
END IF;
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
timeout_ms = 1000;
ELSE
timeout_ms = TG_ARGV[4]::integer;
END IF;
CASE
WHEN method = 'GET' THEN
SELECT http_get INTO request_id FROM net.http_get(
url,
params,
headers,
timeout_ms
);
WHEN method = 'POST' THEN
payload = jsonb_build_object(
'old_record', OLD,
'record', NEW,
'type', TG_OP,
'table', TG_TABLE_NAME,
'schema', TG_TABLE_SCHEMA
);
SELECT http_post INTO request_id FROM net.http_post(
url,
payload,
params,
headers,
timeout_ms
);
ELSE
RAISE EXCEPTION 'method argument % is invalid', method;
END CASE;
INSERT INTO supabase_functions.hooks
(hook_table_id, hook_name, request_id)
VALUES
(TG_RELID, TG_NAME, request_id);
RETURN NEW;
END
$function$;
-- Supabase super admin
DO
$$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_roles
WHERE rolname = 'supabase_functions_admin'
)
THEN
CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
END IF;
END
$$;
GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;
ALTER USER supabase_functions_admin SET search_path = "supabase_functions";
ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin;
ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin;
ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin;
GRANT supabase_functions_admin TO postgres;
-- Remove unused supabase_pg_net_admin role
DO
$$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_roles
WHERE rolname = 'supabase_pg_net_admin'
)
THEN
REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;
DROP OWNED BY supabase_pg_net_admin;
DROP ROLE supabase_pg_net_admin;
END IF;
END
$$;
-- pg_net grants when extension is already enabled
DO
$$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_extension
WHERE extname = 'pg_net'
)
THEN
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
END IF;
END
$$;
-- Event trigger for pg_net
CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_event_trigger_ddl_commands() AS ev
JOIN pg_extension AS ext
ON ev.objid = ext.oid
WHERE ext.extname = 'pg_net'
)
THEN
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
END IF;
END;
$$;
COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';
DO
$$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_event_trigger
WHERE evtname = 'issue_pg_net_access'
) THEN
CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')
EXECUTE PROCEDURE extensions.grant_pg_net_access();
END IF;
END
$$;
INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');
ALTER function supabase_functions.http_request() SECURITY DEFINER;
ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;
COMMIT;

View File

@ -1,16 +0,0 @@
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { serve } from "https://deno.land/std@0.177.1/http/server.ts"
serve(async () => {
return new Response(
`"Hello from Edge Functions!"`,
{ headers: { "Content-Type": "application/json" } },
)
})
// To invoke:
// curl 'http://localhost:<KONG_HTTP_PORT>/functions/v1/hello' \
// --header 'Authorization: Bearer <anon/service_role API key>'

View File

@ -1,94 +0,0 @@
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts'
console.log('main function started')
const JWT_SECRET = Deno.env.get('JWT_SECRET')
const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'
function getAuthToken(req: Request) {
const authHeader = req.headers.get('authorization')
if (!authHeader) {
throw new Error('Missing authorization header')
}
const [bearer, token] = authHeader.split(' ')
if (bearer !== 'Bearer') {
throw new Error(`Auth header is not 'Bearer {token}'`)
}
return token
}
async function verifyJWT(jwt: string): Promise<boolean> {
const encoder = new TextEncoder()
const secretKey = encoder.encode(JWT_SECRET)
try {
await jose.jwtVerify(jwt, secretKey)
} catch (err) {
console.error(err)
return false
}
return true
}
serve(async (req: Request) => {
if (req.method !== 'OPTIONS' && VERIFY_JWT) {
try {
const token = getAuthToken(req)
const isValidJWT = await verifyJWT(token)
if (!isValidJWT) {
return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
} catch (e) {
console.error(e)
return new Response(JSON.stringify({ msg: e.toString() }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
}
const url = new URL(req.url)
const { pathname } = url
const path_parts = pathname.split('/')
const service_name = path_parts[1]
if (!service_name || service_name === '') {
const error = { msg: 'missing function name in request' }
return new Response(JSON.stringify(error), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
const servicePath = `/home/deno/functions/${service_name}`
console.error(`serving the request with ${servicePath}`)
const memoryLimitMb = 150
const workerTimeoutMs = 1 * 60 * 1000
const noModuleCache = false
const importMapPath = null
const envVarsObj = Deno.env.toObject()
const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]])
try {
const worker = await EdgeRuntime.userWorkers.create({
servicePath,
memoryLimitMb,
workerTimeoutMs,
noModuleCache,
importMapPath,
envVars,
})
return await worker.fetch(req)
} catch (e) {
const error = { msg: e.toString() }
return new Response(JSON.stringify(error), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
}
})

View File

@ -1,232 +0,0 @@
api:
enabled: true
address: 0.0.0.0:9001
sources:
docker_host:
type: docker_logs
exclude_containers:
- supabase-vector
transforms:
project_logs:
type: remap
inputs:
- docker_host
source: |-
.project = "default"
.event_message = del(.message)
.appname = del(.container_name)
del(.container_created_at)
del(.container_id)
del(.source_type)
del(.stream)
del(.label)
del(.image)
del(.host)
del(.stream)
router:
type: route
inputs:
- project_logs
route:
kong: '.appname == "supabase-kong"'
auth: '.appname == "supabase-auth"'
rest: '.appname == "supabase-rest"'
realtime: '.appname == "supabase-realtime"'
storage: '.appname == "supabase-storage"'
functions: '.appname == "supabase-functions"'
db: '.appname == "supabase-db"'
# Ignores non nginx errors since they are related with kong booting up
kong_logs:
type: remap
inputs:
- router.kong
source: |-
req, err = parse_nginx_log(.event_message, "combined")
if err == null {
.timestamp = req.timestamp
.metadata.request.headers.referer = req.referer
.metadata.request.headers.user_agent = req.agent
.metadata.request.headers.cf_connecting_ip = req.client
.metadata.request.method = req.method
.metadata.request.path = req.path
.metadata.request.protocol = req.protocol
.metadata.response.status_code = req.status
}
if err != null {
abort
}
# Ignores non nginx errors since they are related with kong booting up
kong_err:
type: remap
inputs:
- router.kong
source: |-
.metadata.request.method = "GET"
.metadata.response.status_code = 200
parsed, err = parse_nginx_log(.event_message, "error")
if err == null {
.timestamp = parsed.timestamp
.severity = parsed.severity
.metadata.request.host = parsed.host
.metadata.request.headers.cf_connecting_ip = parsed.client
url, err = split(parsed.request, " ")
if err == null {
.metadata.request.method = url[0]
.metadata.request.path = url[1]
.metadata.request.protocol = url[2]
}
}
if err != null {
abort
}
# Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency.
auth_logs:
type: remap
inputs:
- router.auth
source: |-
parsed, err = parse_json(.event_message)
if err == null {
.metadata.timestamp = parsed.time
.metadata = merge!(.metadata, parsed)
}
# PostgREST logs are structured so we separate timestamp from message using regex
rest_logs:
type: remap
inputs:
- router.rest
source: |-
parsed, err = parse_regex(.event_message, r'^(?P<time>.*): (?P<msg>.*)$')
if err == null {
.event_message = parsed.msg
.timestamp = to_timestamp!(parsed.time)
.metadata.host = .project
}
# Realtime logs are structured so we parse the severity level using regex (ignore time because it has no date)
realtime_logs:
type: remap
inputs:
- router.realtime
source: |-
.metadata.project = del(.project)
.metadata.external_id = .metadata.project
parsed, err = parse_regex(.event_message, r'^(?P<time>\d+:\d+:\d+\.\d+) \[(?P<level>\w+)\] (?P<msg>.*)$')
if err == null {
.event_message = parsed.msg
.metadata.level = parsed.level
}
# Storage logs may contain json objects so we parse them for completeness
storage_logs:
type: remap
inputs:
- router.storage
source: |-
.metadata.project = del(.project)
.metadata.tenantId = .metadata.project
parsed, err = parse_json(.event_message)
if err == null {
.event_message = parsed.msg
.metadata.level = parsed.level
.metadata.timestamp = parsed.time
.metadata.context[0].host = parsed.hostname
.metadata.context[0].pid = parsed.pid
}
# Postgres logs some messages to stderr which we map to warning severity level
db_logs:
type: remap
inputs:
- router.db
source: |-
.metadata.host = "db-default"
.metadata.parsed.timestamp = .timestamp
parsed, err = parse_regex(.event_message, r'.*(?P<level>INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC?):.*', numeric_groups: true)
if err != null || parsed == null {
.metadata.parsed.error_severity = "info"
}
if parsed != null {
.metadata.parsed.error_severity = parsed.level
}
if .metadata.parsed.error_severity == "info" {
.metadata.parsed.error_severity = "log"
}
.metadata.parsed.error_severity = upcase!(.metadata.parsed.error_severity)
sinks:
logflare_auth:
type: 'http'
inputs:
- auth_logs
encoding:
codec: 'json'
method: 'post'
request:
retry_max_duration_secs: 10
uri: 'http://analytics:4000/api/logs?source_name=gotrue.logs.prod&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}'
logflare_realtime:
type: 'http'
inputs:
- realtime_logs
encoding:
codec: 'json'
method: 'post'
request:
retry_max_duration_secs: 10
uri: 'http://analytics:4000/api/logs?source_name=realtime.logs.prod&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}'
logflare_rest:
type: 'http'
inputs:
- rest_logs
encoding:
codec: 'json'
method: 'post'
request:
retry_max_duration_secs: 10
uri: 'http://analytics:4000/api/logs?source_name=postgREST.logs.prod&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}'
logflare_db:
type: 'http'
inputs:
- db_logs
encoding:
codec: 'json'
method: 'post'
request:
retry_max_duration_secs: 10
# We must route the sink through kong because ingesting logs before logflare is fully initialised will
# lead to broken queries from studio. This works by the assumption that containers are started in the
# following order: vector > db > logflare > kong
uri: 'http://kong:8000/analytics/v1/api/logs?source_name=postgres.logs&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}'
logflare_functions:
type: 'http'
inputs:
- router.functions
encoding:
codec: 'json'
method: 'post'
request:
retry_max_duration_secs: 10
uri: 'http://analytics:4000/api/logs?source_name=deno-relay-logs&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}'
logflare_storage:
type: 'http'
inputs:
- storage_logs
encoding:
codec: 'json'
method: 'post'
request:
retry_max_duration_secs: 10
uri: 'http://analytics:4000/api/logs?source_name=storage.logs.prod.2&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}'
logflare_kong:
type: 'http'
inputs:
- kong_logs
- kong_err
encoding:
codec: 'json'
method: 'post'
request:
retry_max_duration_secs: 10
uri: 'http://analytics:4000/api/logs?source_name=cloudflare.logs.prod&api_key=${LOGFLARE_API_KEY?LOGFLARE_API_KEY is required}'

View File

@ -3,6 +3,8 @@ module.exports = {
content: [ content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/**/*.{js,jsx,ts,tsx}",
"./node_modules/react-tailwindcss-select/dist/index.esm.js"
], ],
theme: { theme: {
extend: { extend: {
@ -14,31 +16,27 @@ module.exports = {
}, },
aspectRatio: {}, // enable aspect-ratio plugin aspectRatio: {}, // enable aspect-ratio plugin
textShadow: { textShadow: {
'pink-glow': '0 0 4px #524FFD, 0 0 4px #524FFD, 0 0 4px #524FFD, 0 0 4px #524FFD', // 'pink-glow': '0 0 4px #524FFD, 0 0 4px #524FFD, 0 0 4px #524FFD, 0 0 4px #524FFD',
'purple-grey-glow': '0 0 4px #9f7aea, 0 0 15px #9f7aea, 0 0 20px #9f7aea, 0 0 25px #9f7aea',
'green-grey-glow': '0 0 4px #32CD32, 0 0 15px #32CD32, 0 0 20px #32CD32, 0 0 25px #32CD32',
}, },
colors: { colors: {
'primary': 'var(--color-primary)',
"neroshi-blue": { 'primary-light': 'var(--color-primary-light)',
50: "#EBEBFF", 'primary-dark': 'var(--color-primary-dark)',
100: "#D2D2FE", 'secondary': 'var(--color-secondary)',
200: "#A6A4FE", 'secondary-light': 'var(--color-secondary-light)',
300: "#7E7CFD", 'secondary-dark': 'var(--color-secondary-dark)',
400: "#524FFD", 'error': 'var(--color-error)',
500: "#2522FC", 'error-light': 'var(--color-error-light)',
600: "#0703E2", 'error-dark': 'var(--color-error-dark)',
700: "#0502AB", 'success': 'var(--color-success)',
800: "#03026F", 'success-light': 'var(--color-success-light)',
900: "#020137", 'success-dark': 'var(--color-success-dark)',
950: "#01001E" 'warning': 'var(--color-warning)',
}, 'warning-light': 'var(--color-warning-light)',
background: "hsl(var(--background))", 'warning-dark': 'var(--color-warning-dark)',
foreground: "hsl(var(--foreground))", 'info': 'var(--color-info)',
btn: { 'info-light': 'var(--color-info-light)',
background: "hsl(var(--btn-background))", 'info-dark': 'var(--color-info-dark)',
"background-hover": "hsl(var(--btn-background-hover))",
},
}, },
}, },
}, },
@ -47,4 +45,4 @@ module.exports = {
require('tailwindcss-textshadow'), require('tailwindcss-textshadow'),
require('@tailwindcss/aspect-ratio') require('@tailwindcss/aspect-ratio')
], ],
}; };