Compare commits

..

No commits in common. "main" and "9b23de08276e1e7fac28b8f52b8e533ccc3ee14a" have entirely different histories.

23 changed files with 5115 additions and 2118 deletions

2
.gitignore vendored
View File

@ -32,5 +32,3 @@ yarn-error.log*
# vercel
.vercel
*.sw*

View File

@ -1,18 +0,0 @@
{
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [
"*"
]
},
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3::twill-wedding-photos/*"
]
}
]
}

View File

@ -1,41 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import { Box } from '@chakra-ui/react';
import { decode } from "blurhash";
function BlurrableImage({ src, thumbSrc, blurHash, isImage, isVideo, onClick }) {
const [loaded, setLoaded] = useState(false);
const canvasRef = useRef();
useEffect(() => {
if (blurHash) {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(200, 200);
const pixels = decode(blurHash, 200, 200);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
}
}, [blurHash]);
if (isImage || !isVideo)
return (
<>
<img src={src} onLoad={e => setLoaded(true)} style={{display:'none'}} alt='Loader' />
{loaded &&
<Box w="100%" h="10" bg="blue.500" bg={`url(${src})`} bgPosition='center' bgSize='cover' onClick={onClick} />
}
{blurHash &&
<canvas width={200} height={200} ref={canvasRef} style={{display: !loaded ? 'block' : 'none'}}/>
}
</>
);
if (isVideo)
return (
<Box w="100%" h="10" bg="blue.500" bg={`url(${thumbSrc})`} bgPosition='center' bgSize='cover' onClick={onClick} d='flex' flexDirection='vertical'>
<div style={{fontSize: 20}}></div>
</Box>
);
return null;
}
export default BlurrableImage;

View File

@ -1,208 +0,0 @@
import React, { useRef } from 'react';
import Link from 'next/link'
import { Box, Button } from "@chakra-ui/react"
import { AddIcon } from '@chakra-ui/icons';
import { encode } from "blurhash";
import useStore from './store';
function Header() {
const uploader = useRef(null);
const { ownerToken, firstName, loggedIn, setLoggedIn, authToken, setAuthToken, addPhoto, setIsUploading, myPhotos, setMyPhotos } = useStore();
const logout = () => {
if (window.confirm('Really logout?')) {
setAuthToken('');
setLoggedIn(false);
}
}
const resizeImage = file => {
if (window.File && window.FileReader && window.FileList && window.Blob) {
return new Promise((resolve, reject) => {
var reader = new FileReader();
reader.onload = function(e) {
var img = new Image();
img.onload = () => {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
var MAX_WIDTH = 400;
var MAX_HEIGHT = 400;
var width = img.width;
var height = img.height;
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const hash = encode(imageData.data, imageData.width, imageData.height, 4, 4);
const dataurl = canvas.toDataURL(file.type);
canvas.toBlob(function(blob) {
const resizedFile = new File([blob], file.name);
console.log(resizedFile);
resolve({ resizedFile, blurHash: hash });
}, 'image/jpeg', 1);
}
img.src = e.target.result;
}
reader.readAsDataURL(file);
});
} else {
alert('The File APIs are not fully supported in this browser.');
}
}
const getImageData = file => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
var image = new Image;
const myPromise = new Promise((resolve, reject) => {
image.onload = () => {
canvas.width = image.width;
canvas.height = image.height;
context.drawImage(image, 0, 0);
resolve(context.getImageData(0, 0, image.width, image.height));
}
});
image.src = URL.createObjectURL(file);
return myPromise;
};
const getVideoCover = (file, seekTo = 0.0) => {
return new Promise((resolve, reject) => {
// load the file to a video player
const videoPlayer = document.createElement('video');
videoPlayer.setAttribute('src', URL.createObjectURL(file));
videoPlayer.load();
videoPlayer.addEventListener('error', (ex) => {
reject("error when loading video file", ex);
});
// load metadata of the video to get video duration and dimensions
videoPlayer.addEventListener('loadedmetadata', () => {
// seek to user defined timestamp (in seconds) if possible
if (videoPlayer.duration < seekTo) {
reject("video is too short.");
return;
}
// delay seeking or else 'seeked' event won't fire on Safari
setTimeout(() => {
videoPlayer.currentTime = seekTo;
}, 200);
// extract video thumbnail once seeking is complete
videoPlayer.addEventListener('seeked', () => {
console.log('video is now paused at %ss.', seekTo);
// define a canvas to have the same dimension as the video
const canvas = document.createElement("canvas");
canvas.width = videoPlayer.videoWidth;
canvas.height = videoPlayer.videoHeight;
// draw the video frame to canvas
const ctx = canvas.getContext("2d");
ctx.drawImage(videoPlayer, 0, 0, canvas.width, canvas.height);
// return the canvas image as a blob
ctx.canvas.toBlob(
blob => {
resolve(blob);
},
"image/jpeg",
0.75 /* quality */
);
});
});
});
}
const uploadFile = async (file) => {
if (!file) return;
const isImage = file.type.split('/')[0] === 'image';
const isVideo = file.type.split('/')[0] === 'video';
if (!isImage && !isVideo) return alert('Please select an image or video.');
let blurHash, resizedFile;
if (isImage) {
const resizeData = await resizeImage(file);
blurHash = resizeData.blurHash;
resizedFile = resizeData.resizedFile;
if (!blurHash || !resizedFile) return;
}
let videoThumb;
if (isVideo) {
videoThumb = await getVideoCover(file, 1.5);
}
const uploadRequestResponse = await fetch(`/api/upload?name=${file.name}&type=${file.type}`, {
headers: { Authorization: authToken },
});
if (uploadRequestResponse.ok) {
const responseJson = await uploadRequestResponse.json();
// If server offers chance to upload original file, then do so in background
if (responseJson.originalUrl) {
fetch(responseJson.originalUrl, { method: 'PUT', body: file });
}
if (responseJson.thumbUrl) {
fetch(responseJson.thumbUrl, { method: 'PUT', body: videoThumb });
}
const uploadResponse = await fetch(responseJson.url, {
method: 'PUT',
body: resizedFile || file,
});
if (uploadResponse.ok) {
const storeResponse = await fetch('/api/photos', {
method: 'POST',
headers: {
Authorization: authToken,
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({ ownerToken, firstName, fileName: responseJson.fileName, blurHash, isImage, isVideo, originalFileName: responseJson.originalFileName, thumbFileName: responseJson.thumbFileName }),
});
const storeResponseJson = await storeResponse.json();
if (storeResponseJson?.url) {
addPhoto({ firstName, fileName: responseJson.fileName, src: storeResponseJson.url, thumbSrc: storeResponseJson.thumbUrl, blurHash, isImage, isVideo });
myPhotos.push(responseJson.fileName);
setMyPhotos(myPhotos);
window.localStorage.setItem('myPhotos', JSON.stringify({ photos: myPhotos }));
}
}
}
}
const handleFileChosen = async (event) => {
const files = event.target?.files;
if (files?.length > 0) {
setIsUploading(true);
for (let i = 0; i < files.length; i++) {
await uploadFile(files[i]);
}
setIsUploading(false);
}
}
return (
<Box bg="teal.400" w="100%" p={4} color="white" d='flex' justifyContent='space-between' alignItems='center'>
<img src='/logo.png' style={{height: 50}} onClick={logout} />
{loggedIn && <>
<Button onClick={() => uploader?.current?.click()} leftIcon={<AddIcon />} colorScheme="teal" variant="solid">
Add a photo or video
</Button>
<input multiple type='file' style={{display:'none'}} ref={uploader} onChange={handleFileChosen} accept='image/*,video/*' />
</>}
</Box>
);
}
export default Header;

View File

@ -1,20 +0,0 @@
import React from 'react';
import Head from 'next/head'
import { Box } from '@chakra-ui/react';
import Header from './Header';
function Layout( { children }) {
return (
<Box d='flex' flexDirection='column' style={{height: '100vh'}}>
<Head>
<title>Twill's Wedding</title>
<link rel="icon" href="/favicon.png" />
</Head>
<Header />
{children}
</Box>
);
}
export default Layout;

View File

@ -1,63 +0,0 @@
import React from 'react';
import { Box, Button, Heading, Text, Input, Alert, AlertIcon } from "@chakra-ui/react"
import styled from 'styled-components';
import Layout from '../lib/Layout';
import useStore from '../lib/store';
const StyledInput = styled(Input)`
::placeholder {
color: white;
}
`;
function LoginPage( { } ) {
const { firstName, setFirstName, password, setPassword, setLoggedIn, setAuthToken, isLoggingIn, setLoggingIn, loginError, setLoginError } = useStore();
const login = async () => {
if (!firstName) return setLoginError('Please enter your first name.');
setLoggingIn(true);
setLoginError('');
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({ password }),
});
setLoggingIn(false);
const respJson = await response.json();
if (respJson.success) {
setLoggedIn(true);
setAuthToken(respJson.token);
}
else {
setLoginError('Unable to login. Is your password correct?');
}
}
return (
<Layout>
<Box pt={30} p={5} bg='teal.400' flex={1} d='flex' flexDirection='column'>
<Heading size='lg' mt={30} color='white'>👋 Welcome!</Heading>
<Text mt={5} color='white'>Tom and Will are getting married! 🤵🏽🤵🏼 </Text>
<Text mt={3} color='white'>We'd love for you to be involved by sharing pictures and videos to help us celebrate. If you have nice (or funny!) pictures of us, or would like to send a video message, or even if you just want to see what others have shared, then <b>login below</b>.</Text>
<Heading size='sm' mt={10} mb={1} color='white'>What is your first name?</Heading>
<StyledInput autoFocus variant='flushed' color='white' colorScheme='teal' placeholder="Your first name..." size="lg" value={firstName} onChange={e => setFirstName(e.target.value)} />
<Heading size='sm' mt={5} mb={1} color='white'>Enter the password</Heading>
<StyledInput variant='flushed' color='white' colorScheme='teal' placeholder="Type your password..." size="lg" type='password' value={password} onChange={e => setPassword(e.target.value)} />
{loginError &&
<Alert status="error" mt={5}>
<AlertIcon /> {loginError}
</Alert>
}
{password?.length > 0 &&
<Button isLoading={isLoggingIn} size='lg' colorScheme='teal' mt={10} onClick={login}>Login</Button>
}
</Box>
</Layout>
);
}
export default LoginPage;

View File

@ -1,37 +0,0 @@
import create from 'zustand'
const useStore = create(set => ({
loggedIn: false,
setLoggedIn: (loggedIn) => set(state => ({ loggedIn })),
authToken: '',
setAuthToken: (authToken) => set(state => ({ authToken })),
ownerToken: '',
setOwnerToken: (ownerToken) => set(state => ({ ownerToken })),
isLoggingIn: false,
setLoggingIn: (isLoggingIn) => set(state => ({ isLoggingIn })),
loginError: '',
setLoginError: (loginError) => set(state => ({ loginError })),
firstName: '',
setFirstName: (firstName) => set(state => ({ firstName })),
password: '',
setPassword: (password) => set(state => ({ password })),
isUploading: false,
uploadingCount: 0,
setIsUploading: (isUploading) => set(state => ({ isUploading, uploadingCount: state.uploadingCount + isUploading ? 1 : -1 })),
selectedPhotoIndex: -1,
setSelectedPhotoIndex: (index) => set(state => ({ selectedPhotoIndex: index })),
myPhotos: [],
setMyPhotos: myPhotos => set(state => ({ myPhotos })),
photos: [],
setPhotos: (photos) => set(state => ({ photos })),
addPhoto: (photo) => set(state => {
const photos = state.photos;
photos.splice(0, 0, photo);
return { photos };
}),
nextPhoto: null,
setNextPhoto: (nextPhoto) => set(state => ({ nextPhoto })),
endReached: false,
setEndReached: (endReached) => set(state => ({ endReached })),
}));
export default useStore;

4854
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,20 +10,10 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.12.0",
"@aws-sdk/s3-request-presigner": "^3.12.0",
"@chakra-ui/icons": "^1.0.9",
"@chakra-ui/react": "^1.5.0",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"blurhash": "^1.1.3",
"faunadb": "^4.1.3",
"framer-motion": "^4",
"jsonwebtoken": "^8.5.1",
"next": "10.1.3",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-infinite-scroll-component": "^6.1.0",
"styled-components": "^5.3.0",
"uuid": "^8.3.2",
"zustand": "^3.4.1"
"uuid": "^8.3.2"
}
}

BIN
pages/.index.js.swp Normal file

Binary file not shown.

View File

@ -1,11 +1,7 @@
import { ChakraProvider } from "@chakra-ui/react"
import '../styles/globals.css'
function MyApp({ Component, pageProps }) {
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
);
return <Component {...pageProps} />
}
export default MyApp

BIN
pages/api/.photos.js.swp Normal file

Binary file not shown.

BIN
pages/api/.upload.js.swp Normal file

Binary file not shown.

View File

@ -1,17 +0,0 @@
import jwt from 'jsonwebtoken';
export default async (req, res) => {
if (req.method == 'POST') {
const { password } = req.body;
if (password === process.env.PUBLIC_PASSWORD) {
const token = jwt.sign({ audience: 'public' }, process.env.JWT_SECRET, { expiresIn: 2592000 });
res.status(200).json({ success: true, token });
} else if (password === process.env.PRIVATE_PASSWORD) {
const token = jwt.sign({ audience: 'private' }, process.env.JWT_SECRET, { expiresIn: 2592000 });
res.status(200).json({ success: true, token });
}
else {
res.status(401).json({ success: false });
}
}
}

View File

@ -1,97 +1,35 @@
import React from 'react';
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import faunadb from 'faunadb';
import jwt from 'jsonwebtoken';
const q = faunadb.query;
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
credentials: {
accessKeyId: process.env.ACCESS_KEY_ID,
secretAccessKey: process.env.SECRET_ACCESS_KEY,
},
});
function getAudience(req, res) {
if (req.headers['authorization']) {
try {
const decoded = jwt.verify(req.headers['authorization'], process.env.JWT_SECRET);
return decoded.audience;
} catch(err) {
console.log(err);
res.status(403).json({ message: 'Invalid token' });
}
}
else res.status(403).json({ message: 'Authorization required' });
}
export default async (req, res) => {
const audience = getAudience(req, res);
if (!audience) return;
if (req.method === 'GET') {
const itemsPerPage = 18;
const params = req.query;
const next = req.query && parseInt(req.query.next);
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
});
const client = new faunadb.Client({ secret: process.env.FAUNA_SECRET })
const helper = await client.query(
q.Paginate(
var helper = await client.paginate(
q.Match(
q.Index("reverse_photos")
),
{
size: itemsPerPage,
after: next ? [next] : undefined,
}
q.Index('all_photos')
)
);
const baseUrl = `https://${process.env.S3_BUCKET}.eu-central-1.linodeobjects.com/`;
const photos = helper.data.map(photo => ({
ts: photo[0],
audience: photo[1],
firstName: photo[2],
fileName: photo[3],
src: `${baseUrl}${photo[3]}`,
origianlFileName: photo[4],
originalSrc: photo[4] ? `${baseUrl}${photo[4]}` : null,
thumbFileName: photo[5],
thumbSrc: photo[5] ? `${baseUrl}${photo[5]}` : null,
blurHash: photo[6],
isImage: photo[7],
isVideo: photo[8],
refId: photo[9].id,
const photos = [];
await helper.map(ref => {
return q.Get(ref)
}).each(page => {
page.forEach(photo => {
photos.push(photo);
});
});
const fileNames = await Promise.all(photos.map(photo => {
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: photo.data.fileName,
});
return getSignedUrl(s3, command);
}));
res.status(200).json({ photos, next: helper.after[0], end: !helper.after});
}
if (req.method == 'POST') {
const client = new faunadb.Client({ secret: process.env.FAUNA_SECRET });
const { ownerToken, firstName, fileName, originalFileName, thumbFileName, blurHash, isImage, isVideo } = req.body;
const createP = await client.query(
q.Create(
q.Collection('photos'),
{ data: { audience, ownerToken, firstName, fileName, originalFileName, thumbFileName, blurHash, isImage, isVideo } }
)
);
res.status(200).json({ url: `https://${process.env.S3_BUCKET}.eu-central-1.linodeobjects.com/${fileName}`, thumbUrl: `https://${process.env.S3_BUCKET}.eu-central-1.linodeobjects.com/${thumbFileName}` });
}
if (req.method == 'DELETE') {
const { ownerToken, id } = req.query;
const client = new faunadb.Client({ secret: process.env.FAUNA_SECRET });
const result = await client.query(
q.Get(q.Ref(q.Collection('photos'), id))
);
if (result && result.data.ownerToken && ownerToken && ownerToken === result.data.ownerToken) {
await client.query(
q.Delete(q.Ref(q.Collection('photos'), id))
);
}
res.status(200).json({ deleted: id });
res.status(200).json({ photos: fileNames });
}
}

View File

@ -1,71 +1,33 @@
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { v4 as uuidv4 } from 'uuid';
import jwt from 'jsonwebtoken';
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
credentials: {
accessKeyId: process.env.ACCESS_KEY_ID,
secretAccessKey: process.env.SECRET_ACCESS_KEY,
},
});
import faunadb from 'faunadb';
const q = faunadb.query;
export default async (req, res) => {
if (req.method === 'GET') {
const headers = req.headers;
let audience;
if (req.headers['authorization']) {
try {
const decoded = jwt.verify(req.headers['authorization'], process.env.JWT_SECRET);
audience = decoded.audience;
} catch(err) {
return res.status(403).json({ message: 'Invalid token' });
}
}
if (!audience) return res.status(403).json({ message: 'Authorization required' });
const params = req.query;
const extension = params.name.split('.').pop();
const id = uuidv4();
const fileName = id + '.' + extension;
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
});
const fileName = uuidv4() + '.jpg';
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: fileName,
ACL: 'public',
ContentType: params.type,
ContentType: 'image/jpg',
});
const url = await getSignedUrl(s3, command);
const returnData = { url, fileName };
// If image return upload URL to optionally upload full-res version
if (params.type.split('/')[0] === 'image') {
const fullFileName = id + '_original.' + extension;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: fullFileName,
ACL: 'public',
ContentType: params.type,
});
const fullUrl = await getSignedUrl(s3, command);
returnData.originalUrl = fullUrl;
returnData.originalFileName = fullFileName;
res.status(200).json({ url, fileName });
}
if (params.type.split('/')[0] === 'video') {
const thumbFileName = id + '_thumb.jpg';
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: thumbFileName,
ACL: 'public',
ContentType: params.type,
});
const thumbUrl = await getSignedUrl(s3, command);
returnData.thumbUrl = thumbUrl;
returnData.thumbFileName = thumbFileName;
}
res.status(200).json(returnData);
if (req.method == 'POST') {
const client = new faunadb.Client({ secret: process.env.FAUNA_SECRET });
const createP = await client.query(
q.Create(
q.Collection('photos'),
{ data: { fileName: req.body.fileName } }
)
);
res.status(200);
}
}

View File

@ -1,159 +1,79 @@
import React, { useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { Text, Grid, GridItem, Box, AspectRatio, Progress, CircularProgress } from '@chakra-ui/react';
import InfiniteScroll from 'react-infinite-scroll-component';
import Layout from '../lib/Layout';
import LoginPage from '../lib/Login';
import BlurrableImage from '../lib/BlurrableImage';
import useStore from '../lib/store';
import React, { useState } from 'react';
import Head from 'next/head'
import faunadb from 'faunadb';
export default function Home({ }) {
const { firstName, setFirstName, loggedIn, setLoggedIn, authToken, setAuthToken, ownerToken, setOwnerToken, isUploading, uploadingCount, selectedPhotoIndex, setSelectedPhotoIndex, photos, setPhotos, addPhoto, myPhotos, setMyPhotos, nextPhoto, setNextPhoto, endReached, setEndReached } = useStore();
const q = faunadb.query;
const getPhotos = async (refresh) => {
if (loggedIn && authToken) {
const response = await fetch(`/api/photos${(!refresh && nextPhoto) ? `?next=${nextPhoto}` : ''}`, {
headers: {
Authorization: authToken,
},
export default function Home({ hello, customers }) {
const [photos, setPhotos] = useState([]);
const getStuff = async (event) => {
const file = event.target?.files[0];
console.log(file);
const response = await fetch('/api/upload');
if (response.ok) { // if HTTP-status is 200-299
const responseJson = await response.json();
const uploadResponse = await fetch(responseJson.url, {
method: 'PUT',
body: file,
});
const json = await response.json();
setPhotos(refresh ? json.photos : photos.concat(json.photos));
if (json.next) setNextPhoto(json.next);
setEndReached(json.end);
}
}
const deletePhoto = async (e, photo) => {
e.stopPropagation();
if (window.confirm('Really delete this photo?')) {
const response = await fetch(`/api/photos?id=${photo.refId}&ownerToken=${ownerToken}`, {
method: 'DELETE',
if (uploadResponse.ok) {
const storeResponse = await fetch('/api/upload', {
method: 'POST',
headers: {
Authorization: authToken,
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({ fileName: responseJson.fileName }),
});
}
}
}
const getPhotos = async () => {
const response = await fetch('/api/photos');
const json = await response.json();
if (json?.deleted) {
setSelectedPhotoIndex(-1);
setPhotos(photos.filter(p => p.refId !== photo.refId));
}
}
setPhotos(json.photos);
console.log(json);
}
// One-time function: load photo URLs from local storage on app-load
useEffect(() => {
const myPhotosString = window.localStorage.getItem('myPhotos');
if (myPhotosString) {
const unencodedString = JSON.parse(myPhotosString);
if (unencodedString?.photos) {
setMyPhotos(unencodedString.photos);
}
}
let storedOwnerToken = window.localStorage.getItem('ownerToken');
if (!storedOwnerToken) {
storedOwnerToken = uuidv4();
window.localStorage.setItem('ownerToken', storedOwnerToken);
}
setOwnerToken(storedOwnerToken);
const storedAuthToken = window.localStorage.getItem('authToken');
if (storedAuthToken) {
setLoggedIn(true);
setAuthToken(storedAuthToken);
}
const storedFirstName = window.localStorage.getItem('firstName');
if (storedFirstName) {
setFirstName(storedFirstName);
}
}, []);
const fetchData = () => {
getPhotos();
}
useEffect(() => {
window.localStorage.setItem('authToken', authToken);
}, [authToken]);
useEffect(() => {
window.localStorage.setItem('firstName', firstName);
}, [firstName]);
// If login status changes then re-fetch photos
useEffect(getPhotos, [loggedIn, authToken]);
if (!loggedIn) return <LoginPage />;
const selectedPhoto = photos && photos[selectedPhotoIndex];
return (
<Layout>
<Box flex={1}>
{isUploading &&
<Progress size="sm" isIndeterminate colorScheme='blue' />
}
{/*
<Grid templateColumns="repeat(3, 1fr)" gap={0.5} flex={1}>*/}
<InfiniteScroll
dataLength={photos.length}
next={fetchData}
hasMore={!endReached}
loader={<h4 style={{textAlign: 'center'}}>Loading...</h4>}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
style={{display: 'grid', gridGap: 2, gridTemplateColumns: 'repeat(3, 1fr)'}}
>
{photos.map((photo, i) =>
<AspectRatio maxW="100%" ratio={1} key={photo.src}>
<BlurrableImage {...photo} onClick={e => setSelectedPhotoIndex(i)}/>
</AspectRatio>
<div>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<h1>Wedding</h1>
<h2>{hello}</h2>
<p>{customers}</p>
<button onClick={getPhotos}>Hi</button>
<input type="file" onChange={getStuff} />
{photos.map(photo =>
<img src={photo} />
)}
</InfiniteScroll>
{/*</Grid>*/}
{selectedPhotoIndex > -1 &&
<div style={{position: 'fixed', top: 0, width: '100%', height: '100%', background: 'rgba(0,0,0,0.8)', display: 'flex', flexDirection: 'column', justifyContent: 'center'}} onClick={e => setSelectedPhotoIndex(-1)}>
{(selectedPhoto.isImage || !selectedPhoto.isVideo) &&
<img src={selectedPhoto.src} style={{maxWidth: '100%', maxHeight: '900px', alignSelf: 'center', display: 'block'}} onClick={e => e.stopPropagation()}/>
}
{selectedPhoto.isVideo &&
<video playsInline style={{maxWidth:'100%', maxHeight: '900px', alignSelf: 'center', display: 'block'}} controls autoPlay onClick={e => e.stopPropagation()}>
<source src={selectedPhoto.src} />
</video>
}
{selectedPhoto.firstName &&
<Text color='white' textAlign='center' fontSize='lg' mt={5}>📸 Added by {selectedPhoto.firstName}</Text>
}
<div style={{position: 'absolute', left: '5px', top: '10px', fontSize: 15, background: 'rgba(0,0,0,0.5)', padding: 4, borderRadius: '50%', width:30, height: 30, textAlign: 'center' }} onClick={e => {setSelectedPhotoIndex(-1);e.stopPropagation();}}>
</div>
{myPhotos?.indexOf(selectedPhoto.fileName) > -1 && selectedPhoto.refId &&
<div style={{position: 'absolute', right: '5px', top: '30%', fontSize: 20, background: 'rgba(0,0,0,0.5)', padding: 4, borderRadius: '50%', width:40, height: 40, textAlign: 'center' }} onClick={e => deletePhoto(e, selectedPhoto)}>
🚮
</div>
}
{selectedPhoto.originalSrc &&
<a href={selectedPhoto.originalSrc} target="_blank" rel="noopener noreferrer" style={{position: 'absolute', right: '5px', top: '40%', fontSize: 20, background: 'rgba(0,0,0,0.5)', padding: 4, borderRadius: '50%', width:40, height: 40, textAlign: 'center' }} onClick={e => e.stopPropagation()}>
🖼
</a>
}
{selectedPhotoIndex > 0 &&
<div style={{position: 'absolute', left: '5px', top: '45%', fontSize: 20, background: 'rgba(0,0,0,0.5)', padding: 4, borderRadius: '50%', width:40, height: 40, textAlign: 'center' }} onClick={e => {setSelectedPhotoIndex(selectedPhotoIndex - 1);e.stopPropagation();}}>
👈
</div>
}
{selectedPhotoIndex < photos.length - 1&&
<div style={{position: 'absolute', right: '5px', top: '45%', fontSize: 20, background: 'rgba(0,0,0,0.5)', padding: 4, borderRadius: '50%', width:40, height: 40, textAlign: 'center' }} onClick={e => {setSelectedPhotoIndex(selectedPhotoIndex + 1);e.stopPropagation();}}>
👉
</div>
}
</div>
}
</Box>
</Layout>
)
}
export async function getStaticProps(context) {
const client = new faunadb.Client({ secret: process.env.FAUNA_SECRET })
var helper = await client.paginate(
q.Match(
q.Index('all_customers')
)
);
const customers = [];
await helper.map(function(ref) {
return q.Get(ref)
})
.each(function(page) {
page.forEach(customer => customers.push(customer));
});
console.log(customers);
return {
props: { hello: 123, }, // will be passed to the page component as props
}
}

View File

@ -1,41 +0,0 @@
import React, { useState } from 'react';
import Head from 'next/head'
import { useRouter } from 'next/router'
import faunadb from 'faunadb';
import Layout from '../lib/Layout';
const q = faunadb.query;
export default function New({ }) {
const router = useRouter()
const getStuff = async (event) => {
const file = event.target?.files[0];
console.log(file);
const response = await fetch('/api/upload');
if (response.ok) { // if HTTP-status is 200-299
const responseJson = await response.json();
const uploadResponse = await fetch(responseJson.url, {
method: 'PUT',
body: file,
});
if (uploadResponse.ok) {
const storeResponse = await fetch('/api/photos', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({ fileName: responseJson.fileName }),
});
router.push('/')
}
}
}
return (
<Layout>
<h1>New</h1>
<input type="file" onChange={getStuff} />
</Layout>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

122
styles/Home.module.css Normal file
View File

@ -0,0 +1,122 @@
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
width: 100%;
height: 100px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
.footer img {
margin-left: 0.5rem;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
.card {
margin: 1rem;
flex-basis: 45%;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h3 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

16
styles/globals.css Normal file
View File

@ -0,0 +1,16 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

1380
yarn.lock

File diff suppressed because it is too large Load Diff