diff --git a/.DS_Store b/.DS_Store index 8eaee294..118da247 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/supabaseStorage.yml b/.github/workflows/supabaseStorage.yml new file mode 100644 index 00000000..71be1c28 --- /dev/null +++ b/.github/workflows/supabaseStorage.yml @@ -0,0 +1,42 @@ +name: SupaStorage-backup +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + schedule: + - cron: '0 8,20 * * *' + + jobs: + backup: + runs-on: ubuntu-latest + env: + SUPABASE_URL: https://qwbufbmxkjfaikoloudl.supabase.co + SUPABASE_SERVICE_ROLE: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InF3YnVmYm14a2pmYWlrb2xvdWRsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Njk5NDE3NTksImV4cCI6MTk4NTUxNzc1OX0.RNz5bvsVwLvfYpZtUjy0vBPcho53_VS2AIVzT8Fm-lk + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ gtihub.head_ref }} + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies and perform backup + run: | + pip install supabase + [[ -d supabase_storage_backup ]] | mkdir supabase_storage_backup + cd supabase_storage_backup + wget https://raw.githubusercontent.com/signal-k/client/main/storage-backup.py + chmod +x storage-backup.py + python storage-backup.py + rm storage-backup.py + shell: bash + + - name: Set current date as env variable + run: echo "NOW=$(date + '%Y -%m-%dT%H:%M:%S')" >> $GITHUB_ENV + - uses: \ No newline at end of file diff --git a/components/AccountAvatar.tsx b/components/AccountAvatar.tsx index 52483555..79767d01 100644 --- a/components/AccountAvatar.tsx +++ b/components/AccountAvatar.tsx @@ -84,11 +84,13 @@ export default function AccountAvatar ({ export function PostCardAvatar ({ url, size, - //onUpload + //uid, + //onUpload, }: { url: Profiles['avatar_url'] - size: number - //onUpload: (url: string) => void + size: number, + //onUpload: (url: string) => void, + //uid: string }) { let width = 'w-12'; //width = 'w-24 md:w-36'; @@ -100,6 +102,33 @@ export function PostCardAvatar ({ if (url) downloadImage(url); }, [url]); + /*const uploadAvatar: React.ChangeEventHandler = async (event) => { + try { + setUploading(true); + if (!event.target.files || event.target.files.length === 0) { // If there is no file selected + throw new Error('You must select an image to upload'); + }; + + const file = event.target.files[0]; + const fileExt = file.name.split('.').pop(); + const fileName = `${uid}.${fileExt}`; + const filePath = `${fileName}`; + let { error: uploadError } = await supabase.storage + .from('avatars') + .upload(filePath, file, { upsert: true }) + if (uploadError) { + throw uploadError; + }; + + onUpload(filePath); + } catch (error) { + alert('Error uploading avatar, check console'); + console.log(error); + } finally { + setUploading(false); + } + }*/ + async function downloadImage(path: string) { // Get the avatar url from Supabase for the user (if it exists) try { const { data, error } = await supabase.storage.from('avatars').download(path); @@ -187,6 +216,91 @@ export function AccountAvatarV1 ({ } } + return ( +
+ {avatarUrl ? ( + Avatar + ) : ( +
+ )} +
+ + +
+
+ ); +} + +export function AccountAvatarV2 ({ + uid, + url, + size, + onUpload +}) { + const supabase = useSupabaseClient(); + const [avatarUrl, setAvatarUrl] = useState(null); + const [uploading, setUploading] = useState(false); + useEffect(() => { + if (url) downloadImage(url); + }, [url]); + + async function downloadImage(path: string) { // Get the avatar url from Supabase for the user (if it exists) + try { + const { data, error } = await supabase.storage.from('avatars').download(path); + if (error) { + throw error; + }; + const url = URL.createObjectURL(data); + setAvatarUrl(url); + } catch (error) { + console.log('Error downloading image: ', error) + } + } + + const uploadAvatar: React.ChangeEventHandler = async (event) => { + try { + setUploading(true); + if (!event.target.files || event.target.files.length === 0) { // If there is no file selected + throw new Error('You must select an image to upload'); + }; + + const file = event.target.files[0]; + const fileExt = file.name.split('.').pop(); + const fileName = `${uid}.${fileExt}`; + const filePath = `${fileName}`; + let { error: uploadError } = await supabase.storage + .from('avatars') + .upload(filePath, file, { upsert: true }) + if (uploadError) { + throw uploadError; + }; + + onUpload(filePath); + } catch (error) { + alert('Error uploading avatar, check console'); + console.log(error); + } finally { + setUploading(false); + } + } + return (
{avatarUrl ? ( diff --git a/components/Core/Footer.tsx b/components/Core/Footer.tsx index dd940ef0..a748b464 100644 --- a/components/Core/Footer.tsx +++ b/components/Core/Footer.tsx @@ -14,4 +14,26 @@ export default function Footer () {
) +} + +export function FooterPlanetPage () { + function editPlanet() { + if (/planets/.test(window.location.href)) { + alert("The URL contains the string 'URL'"); +} + } + + return ( +
+ + + +
+ ) } \ No newline at end of file diff --git a/components/Core/Layout.jsx b/components/Core/Layout.jsx index 2038aa80..1bdd30ef 100644 --- a/components/Core/Layout.jsx +++ b/components/Core/Layout.jsx @@ -6,7 +6,7 @@ import Footer from "./Footer"; export default function CoreLayout ( { children } ) { // Handling responsive UI - const [showNav, setShowNav] = useState(true); + const [showNav, setShowNav] = useState(false); const [isMobile, setIsMobile] = useState(false); function handleResize () { @@ -57,7 +57,7 @@ export default function CoreLayout ( { children } ) { export function GameplayLayout ( { children } ) { // Handling responsive UI - const [showNav, setShowNav] = useState(true); + const [showNav, setShowNav] = useState(false); const [isMobile, setIsMobile] = useState(false); function handleResize () { diff --git a/components/Core/Navigation.tsx b/components/Core/Navigation.tsx index 082246f8..d8a80e15 100644 --- a/components/Core/Navigation.tsx +++ b/components/Core/Navigation.tsx @@ -28,6 +28,9 @@ export default function CoreNavigation ({ showNav, setShowNav }) { const [username, setUsername] = useState(null); const [avatar_url, setAvatarUrl] = useState(null); + const [profileMenuOpen, setProfileMenuOpen] = useState(false); + const toggle = () => setProfileMenuOpen(!profileMenuOpen); + useEffect(() => { getProfile(); }, [session]); @@ -68,7 +71,7 @@ export default function CoreNavigation ({ showNav, setShowNav }) { />
- + {/* @@ -146,71 +149,110 @@ export default function CoreNavigation ({ showNav, setShowNav }) {
- - -
- - - profile picture - - - {username} - - - -
- - -
- - - - Edit - - - - - - ORCID - - - - - - Settings - - - - {/*
*/} - {/* OnClick -> add the returned signature & address to the user's supabase "address" field on table "profiles" */} - -
- - -
+ */} +
+ + {profileMenuOpen && ( +
+ + Profile + + + Settings + + + Sign out + +
+ )} +
+ ); -} \ No newline at end of file +} + +/* + +
+ + + profile picture + + + Profile {/*{username} + + + +
+ + +
+ + + + Edit + + + + + + ORCID + + + + + + Settings + + + + {/* OnClick -> add the returned signature & address to the user's supabase "address" field on table "profiles" + +
+
+
+
+ */ \ No newline at end of file diff --git a/components/Core/Sidebar.jsx b/components/Core/Sidebar.jsx index e3f25b39..48bc0464 100644 --- a/components/Core/Sidebar.jsx +++ b/components/Core/Sidebar.jsx @@ -51,7 +51,23 @@ const CoreSidebar = forwardRef(({ showNav }, ref) => { - + +
+
+ +
+
+

Planets

+
+
+ + {/*
{

ORCID

- +
{

Lens

- + */} ); @@ -143,6 +159,77 @@ export default CoreSidebar; export const GameplaySidebar = forwardRef(({ showNav }, ref) => { const router = useRouter(); + return ( +
+
+ + signal kinetics logo + +
+ +
+ +
+
+ +
+
+

Home

+
+
+ + +
+
+ +
+
+

Planets

+
+
+ + +
+
+ +
+
+

Vote

+
+
+ +
+
+ ); +}); + +CoreSidebar.displayName = "SideBar"; + +export const GameplaySidebarFull = forwardRef(({ showNav }, ref) => { + const router = useRouter(); + return (
@@ -204,6 +291,22 @@ export const GameplaySidebar = forwardRef(({ showNav }, ref) => {
+ +
+
+ +
+
+

Vote

+
+
+
{
); -}); - -CoreSidebar.displayName = "SideBar"; \ No newline at end of file +}); \ No newline at end of file diff --git a/components/Core/UpdateProfile.tsx b/components/Core/UpdateProfile.tsx new file mode 100644 index 00000000..f8d02c4a --- /dev/null +++ b/components/Core/UpdateProfile.tsx @@ -0,0 +1,234 @@ +import React, { useState, useEffect, createRef } from "react"; +import { Container, Form, Button, Row, Col, Card } from "react-bootstrap"; +import Link from "next/link"; + +import { useUser, useSupabaseClient, Session } from "@supabase/auth-helpers-react"; +import { Database } from "../../utils/database.types"; + +import { AccountAvatarV1 } from "../../components/AccountAvatar"; +import { imagesCdnAddress } from "../../constants/cdn"; +import { v4 as uuidv4 } from 'uuid'; + +type Profiles = Database['public']['Tables']['profiles']['Row']; +type Planets = Database['public']['Tables']['planets']['Row']; + +export default function AccountEditor({ session }: { session: Session }) { + const supabase = useSupabaseClient(); + const user = useUser(); + const [loading, setLoading] = useState(true); + const [username, setUsername] = useState(null); + const [website, setWebsite] = useState(null); // I believe this is the email field + const [avatar_url, setAvatarUrl] = useState(null); + const [address2, setAddress2] = useState(null); + const [address, setAddress] = useState(null); // This should be set by the handler eventually (connected address). + const [images, setImages] = useState([]); + + // User planet + const [userIdForPlanet, setUserIdForPlanet] = useState(null); + const [planetGeneratorImage, setPlanetGeneratorImage] = useState(null); + + const ref = createRef(); + let width = '100%' + + useEffect(() => { + getProfile(); + console.log(session?.user?.id) + }, [session]); + + async function getProfile () { + try { + setLoading(true); + if (!user) throw new Error('No user authenticated'); + let { data, error, status } = await supabase + .from('profiles') + .select(`username, website, avatar_url, address, address2`) + .eq('id', user.id) + .single() + + if (error && status !== 406) { + throw error; + } + + if (data) { + setUsername(data.username); + setWebsite(data.website); + setAvatarUrl(data.avatar_url); + setAddress(data.address); + } + } catch (error) { + //alert('Error loading your user data'); + console.log(error); + } finally { + setLoading(false); + } + } + + async function updateProfile({ + username, + website, + avatar_url, + address, + } : { + username: Profiles['username'] + website: Profiles['website'] + avatar_url: Profiles['avatar_url'] + address: Profiles['address'] + }) { + try { + setLoading(true); + if (!user) throw new Error('No user authenticated!'); + const updates = { + id: user.id, + username, + website, + avatar_url, + address, + address2, + updated_at: new Date().toISOString(), + } + let { error } = await supabase.from('profiles').upsert(updates); + if (error) throw error; + alert('Off-chain Profile updated'); + } catch (error) { + alert('Error updating your profile data:'); + console.log(error); + } finally { + setLoading(false); + } + } + + // Gallery components + // Retrieving gallery data for user + async function getImages() { + const { data, error } = await supabase + .storage + .from('images') + .list(user?.id + '/', { + limit: 100, // Get 100 images from this dir + offset: 0, + sortBy: { + column: 'name', + order: 'asc' + } + }); + + if ( data !== null ) { + setImages(data); + } else { + alert('Error loading images'); + console.log(error); + } + } + + async function uploadImage(e) { + let file = e.target.files[0]; + const { data, error } = await supabase + .storage + .from('images') + .upload(user.id + '/' + uuidv4(), file); + + if (data) { + getImages(); + } else { + console.log(error); + } + } + + async function deleteImage (imageName) { + const { error } = await supabase + .storage + .from('images') + .remove([ user.id + '/' + imageName ]) + + if (error) { + alert (error); + } else { + getImages(); + } + } + + useEffect(() => { + if (user) { // Only get images IF the user exists and is logged in + getImages(); // Add a getPosts function to get a user's profile posts + } + }, [user]); + + function convertURIToImageData(URI) { + return new Promise(function(resolve, reject) { + if (URI == null) return reject(); + var canvas = document.createElement('canvas'), + context = canvas.getContext('2d'), + image = new Image(); + image.addEventListener('load', function() { + canvas.width = image.width; + canvas.height = image.height; + context.drawImage(image, 0, 0, canvas.width, canvas.height); + resolve(context.getImageData(0, 0, canvas.width, canvas.height)); + }, false); + image.src = URI; + }); + } + + /* PLANET manipulation */ + async function createPlanet({ // Maybe we should add a getPlanet (getUserPlanet) helper as well? + userId, temperature, radius, date, ticId + } : { + //id: Planets['id'] + userId: Planets['userId'] // Check to see if this page gets the userId as well, or just the username. Foreign key still works regardless + temperature: Planets['temperature'] + radius: Planets['radius'] + date: Planets['date'] + ticId: Planets['ticId'] + }) { + try { + setLoading(true); + // Is the planet ID going to be based on the user id (obviously not in production, but in this version?) + const newPlanetParams = { + id: user.id, // Generate a random id later + // .. other params from database types + } + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + } + + async function getUserPlanet() { + try { + setLoading(true); + if (!user) throw new Error('No user authenticated'); + let { data, error, status } = await supabase + .from('planetsss') + .select(`id, userId, temperature, radius, ticId`) + .eq('userId', username) + .single() + + if (error && status !== 406) { + throw error; + } + + if (data) { + setUserIdForPlanet(data.userId); + } + } catch (error) { + console.log(error); + } finally { + setLoading(false); + } + } + + return ( +
+ { + setAvatarUrl(url) + updateProfile({ username, website, avatar_url: url, address}) + }} + /> +
+ ) +} \ No newline at end of file diff --git a/components/Core/tests/db.tsx b/components/Core/tests/db.tsx new file mode 100644 index 00000000..dc9e3464 --- /dev/null +++ b/components/Core/tests/db.tsx @@ -0,0 +1,161 @@ +export default function Db () { + return ( + <>
+
+
+
+
+ + + + + + + + + +
+

Short length headline.

+

Free and Premium themes, UI Kit's, templates and landing pages built with Tailwind CSS, HTML & Next.js.

+
+
+
+ + + + + + + + + +
+

Short length headline.

+

Free and Premium themes, UI Kit's, templates and landing pages built with Tailwind CSS, HTML & Next.js.

+
+
+
+
+ + + ) +} \ No newline at end of file diff --git a/components/Gameplay/Planets/PlanetCard.tsx b/components/Gameplay/Planets/PlanetCard.tsx index 3e4206be..cbe2202a 100644 --- a/components/Gameplay/Planets/PlanetCard.tsx +++ b/components/Gameplay/Planets/PlanetCard.tsx @@ -7,6 +7,9 @@ import PlanetEditor, { PlanetEditorFromData } from "../../../pages/generator/pla import StakePlay from "../../../pages/stake/play"; import UtterancesComments from "../../Lens/Utterances"; import { useContract, useContractRead, useContractWrite, useLazyMint } from "@thirdweb-dev/react"; +import { planetsImagesCdnAddress } from "../../../constants/cdn"; +import { v4 as uuidv4 } from 'uuid'; +import { Col, Container, Row, Form } from "react-bootstrap"; export function PlanetCard ({ activeTab, planetId }) { const supabase = useSupabaseClient(); @@ -14,13 +17,16 @@ export function PlanetCard ({ activeTab, planetId }) { const session = useSession(); const [planetUri, setPlanetUri] = useState(); const [planetOwner, setPlanetOwner] = useState(null); + const [username, setUsername] = useState(''); + const [images, setImages] = useState([]); + const [playerReputation, setPlayerRepuation] = useState(); function fetchPlanet () { supabase.from('planetsss') .select("*") .eq('id', planetId) // How should the ID be generated -> similar to how `userId` is generated? Combination of user + org + article + dataset number?? .then(result => { if (result.error) { throw result.error; }; - if (result.data) { setPlanet(result.data[0]); /*console.log(planet);*/ setPlanetOwner(planet?.ownerId); }; + if (result.data) { setPlanet(result?.data[0]); /*console.log(planet);*/ setPlanetOwner(planet?.ownerId); }; } ); } @@ -49,12 +55,105 @@ export function PlanetCard ({ activeTab, planetId }) { } } + const updatePlayerReputation = async () => { + let newReputation = playerReputation + 1; + setPlayerRepuation(newReputation); + + try { + const { data, error } = await supabase + .from('profiles') + .update([ + { reputation: newReputation, } + ]) + .eq('id', session?.user?.id); + + if (error) throw error; + } catch (error: any) { + console.log(error); + } + } + + const claimPlanet = async () => { + try { + const { data, error } = await supabase + .from('planetsss') + .update([ + { owner: session?.user?.id, /*userId: username*/ } + ]) + .eq('id', planetId); + updatePlayerReputation(); // Do this for posts, journals as well + + if (error) throw error; + } catch (error: any) { + console.log(error); + } + } + + async function getPlanetImages () { + const { data, error } = await supabase + .storage + .from('planets') + .list(planet?.id + '/', { + limit: 100, + offset: 0, + sortBy: { + column: 'name', + order: 'asc', + } + }); + + if (data !== null) { + setImages(data); + } else { + alert('Error loading images'); + console.log(error); + } + } + + async function uploadImageForPlanet (e) { + let file = e.target.files[0]; + const { data, error } = await supabase + .storage + .from('planets') + .upload(session?.user?.id + '/' + uuidv4(), file); // Also add to media/planet post media bucket + + if (data) { + getPlanetImages(); + } else { + console.log(error); + } + } + + // Get the planet's assets/images + useEffect(() => { + if (planet) { + fetchPlanet(); + console.log(planet); + getPlanetImages(); + console.log(planet?.id); + console.log(planetsImagesCdnAddress + planet?.id + '/' + 'download.png'); + } + }, [planet?.id]); + return (
{activeTab === 'planet' && ( -
- Planet Name -
+
{/* +