matrix-art

An image gallery for Matrix
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-art.git
Log | Files | Refs | README | LICENSE

commit 165be27c1737db92cd10fd38b995c0945b833179
parent e75348762ea8bc4178051a2acbe64e7fe0ec06a2
Author: MTRNord <mtrnord1@gmail.com>
Date:   Tue, 25 Jan 2022 01:11:48 +0100

Add rough Initial Submit page design

Diffstat:
Acomponents/submit_page/main.tsx | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acomponents/submit_page/start.tsx | 37+++++++++++++++++++++++++++++++++++++
Mpackage-lock.json | 1+
Mpackage.json | 1+
Mpages/index.tsx | 2+-
Mpages/login.tsx | 6+++---
Mpages/submit.tsx | 91++++++++++++++++++++++++++++++++++---------------------------------------------
Mstyles/globals.css | 5+++++
8 files changed, 276 insertions(+), 56 deletions(-)

diff --git a/components/submit_page/main.tsx b/components/submit_page/main.tsx @@ -0,0 +1,188 @@ +import { PureComponent } from "react"; +import { DropCallbacks } from "./start"; +import PropTypes from 'prop-types'; +import { PreviewWithDataFile } from "../../pages/submit"; +import { ClientContext } from "../ClientContext"; +import Dropzone from "react-dropzone"; + +interface Props extends DropCallbacks { + files: PreviewWithDataFile[]; +}; + +type State = { + currentFileIndex: number; + hasSubmit: boolean; + hasBack: boolean; + [key: string]: any; +}; +class MainSubmissionForm extends PureComponent<Props, State> { + declare context: React.ContextType<typeof ClientContext>; + + constructor(props: Props) { + super(props); + this.state = { + currentFileIndex: 0, + hasSubmit: props.files.length === 1 ? true : false, + hasBack: false + }; + } + static propTypes = { + onDrop: PropTypes.func, + onDropError: PropTypes.func, + files: PropTypes.array, + hasSubmit: PropTypes.bool, + hasBack: PropTypes.bool + }; + + renderThumb() { + const file = this.props.files[this.state.currentFileIndex]; + return ( + <div className="relative" style={{ height: "280px" }} key={file.name}> + <img alt={file.name} className="h-auto w-full object-cover max-w-full align-bottom" style={{ height: "280px" }} src={file.preview_url} /> + </div> + ); + } + + renderThumbs() { + return this.props.files.map((file, index) => { + const setIndex = () => { this.setState({ currentFileIndex: index }); }; + return ( + <div className="relative cursor-pointer w-auto aspect-video" onClick={setIndex} style={{ height: "140px" }} key={file.name}> + <img alt={file.name} className="h-auto w-full object-cover max-w-full align-bottom aspect-video" style={{ height: "140px" }} src={file.preview_url} /> + </div> + ); + }); + } + + onNext(ev: { preventDefault: () => void; }) { + ev.preventDefault(); + const newIndex = this.state.currentFileIndex + 1; + if (this.props.files.length - 1 > newIndex) { + const hasBack = newIndex > 0 ? true : false; + this.setState({ currentFileIndex: newIndex, hasBack }); + } else if (this.props.files.length - 1 === newIndex) { + this.setState({ currentFileIndex: newIndex, hasSubmit: true }); + } + } + + onPrev(ev: { preventDefault: () => void; }) { + ev.preventDefault(); + const newIndex = this.state.currentFileIndex - 1; + if (this.props.files.length - 1 > newIndex) { + const hasBack = newIndex > 0 ? true : false; + this.setState({ currentFileIndex: newIndex, hasBack }); + } else if (this.props.files.length - 1 === newIndex) { + this.setState({ currentFileIndex: newIndex, hasSubmit: true }); + } + } + + + async handleSubmit(event: { preventDefault: () => void; }) { + const range = [...Array(this.props.files.length - 1).keys()]; // eslint-disable-line unicorn/new-for-builtins + for (const index of range) { + const title = `${index}_title`; + console.log(`${index}: ${this.state[title]}`); + const description = `${index}_description`; + console.log(`${index}: ${this.state[description]}`); + const tags = `${index}_tags`; + console.log(`${index}: ${this.state[tags]}`); + const license = `${index}_license`; + console.log(`${index}: ${this.state[license]}`); + const nsfw = `${index}_nsfw`; + console.log(`${index}: ${this.state[nsfw]}`); + } + } + + handleInputChange(event: { target: any; }) { + const target = event.target; + const value = target.type === 'checkbox' ? target.checked : target.value; + const name = `${this.state.currentFileIndex}_${target.name}`; + this.setState({ + [name]: value + } as State); + } + + render() { + return ( + <main className='min-h-full max-w-full flex flex-col justify-start items-center lg:pt-20 pt-52 z-0 bottom-0 relative mb-8'> + <section className="flex flex-col items-start mb-4"> + <div className="flex flex-row my-4"> + {this.renderThumb()} + </div> + <div className="flex flex-row w-full mb-4"> + <form className="flex flex-col w-full"> + <label className="inner-flex flex-col"> + <span className="text-xl text-gray-900 dark:text-gray-200 font-bold">Image Title</span> + <input required name="title" value={this.state[`${this.state.currentFileIndex}_title`] || ""} type="text" placeholder="Set an image title" className="min-w-full placeholder:text-gray-900 text-gray-900" onChange={this.handleInputChange.bind(this)} /> + </label> + + <span className="h-4"></span> + + <label className="inner-flex flex-col"> + <span className="text-xl text-gray-900 dark:text-gray-200 font-bold">Description</span> + <textarea name="description" value={this.state[`${this.state.currentFileIndex}_description`] || ""} placeholder="Description Text of the current image" className="min-w-full placeholder:text-gray-900 text-gray-900" onChange={this.handleInputChange.bind(this)} /> + </label> + + <span className="h-4"></span> + + <label className="inner-flex flex-col"> + <span className="text-xl text-gray-900 dark:text-gray-200 font-bold">Image Tags</span> + <input name="tags" type="text" value={this.state[`${this.state.currentFileIndex}_tags`] || ""} placeholder="Enter tags (Seperate with a comma)" className="min-w-full placeholder:text-gray-900 text-gray-900" onChange={this.handleInputChange.bind(this)} /> + </label> + + <span className="h-4"></span> + + <label className="inner-flex flex-col"> + <span className="text-xl text-gray-900 dark:text-gray-200 font-bold">License</span> + <select required name="license" value={this.state[`${this.state.currentFileIndex}_license`] || ""} placeholder="Enter tags (Confirm by pressing enter)" className="min-w-full placeholder:text-gray-900 text-gray-900" onChange={this.handleInputChange.bind(this)}> + <option value="" disabled selected>Select an Creative Commons License</option> + </select> + </label> + + <span className="h-4"></span> + + <label className="inner-flex flex-col"> + <span className="text-xl text-gray-900 dark:text-gray-200 font-bold">Mature/NSFW Content?</span> + <select required name="nsfw" value={this.state[`${this.state.currentFileIndex}_nsfw`] || ""} placeholder="Enter tags (Confirm by pressing enter)" className="min-w-full placeholder:text-gray-900 text-gray-900" onChange={this.handleInputChange.bind(this)}> + <option value="" disabled selected>Select Yes or No</option> + <option value="no">No</option> + <option value="yes">Yes</option> + </select> + </label> + </form> + </div> + <div className="flex justify-between w-full flex-row-reverse"> + { + this.state.hasSubmit ? + <button className="text-base text-gray-900 dark:text-gray-200" onClick={this.handleSubmit.bind(this)}>Submit Posts</button> + : + <button className="text-base text-gray-900 dark:text-gray-200" onClick={this.onNext.bind(this)}>Next Image</button> + } + {this.state.hasBack ? <button className="text-base text-gray-900 dark:text-gray-200" onClick={this.onPrev.bind(this)}>Prev Image</button> : undefined} + </div> + </section> + {/* Warning for further readers. This css is easy to explode. Reasons are unknown. Dont touch it unless you know the fix! */} + <section className="max-w-full flex flex-row justify-start items-start "> + <div className="overflow-x-scroll mr-4 ml-8"> + <div className="flex gap-1"> + {this.renderThumbs()} + </div> + </div> + <Dropzone accept='image/*' onDropAccepted={this.props.onDrop} onDropRejected={this.props.onDropError}> + {({ getRootProps, getInputProps }) => ( + <div className="flex aspect-square justify-center items-center border border-dashed dark:border-white border-black cursor-pointer h-[140px] w-[140px] mr-8" {...getRootProps()}> + <input {...getInputProps()} /> + <svg className="dark:fill-gray-200 fill-gray-900" xmlns="http://www.w3.org/2000/svg" height="5rem" viewBox="0 0 24 24" width="5rem" fill="inherit"><path d="M0 0h24v24H0V0z" fill="none" /><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /></svg> + </div> + )} + </Dropzone> + </section> + </main> + ); + } +} + + +MainSubmissionForm.contextType = ClientContext; + +export default MainSubmissionForm; +\ No newline at end of file diff --git a/components/submit_page/start.tsx b/components/submit_page/start.tsx @@ -0,0 +1,36 @@ +import { PureComponent } from "react"; +import Dropzone, { FileRejection } from "react-dropzone"; +import PropTypes from 'prop-types'; + +export interface DropCallbacks { + onDrop: (files: File[]) => void; + onDropError: (fileRejections: FileRejection[]) => void; +} + +interface Props extends DropCallbacks { } + +export class StartSubmit extends PureComponent<Props> { + static propTypes = { + onDrop: PropTypes.func, + onDropError: PropTypes.func + }; + + render() { + return ( + <main className='min-h-full flex flex-col justify-center lg:pt-20 pt-52 z-0 bottom-0 relative flex-grow mb-8'> + <div className="flex justify-center items-center mt-4"> + <Dropzone accept='image/*' onDropAccepted={this.props.onDrop} onDropRejected={this.props.onDropError}> + {({ getRootProps, getInputProps }) => ( + <section className="flex flex-col"> + <div className="dark:bg-[#fefefe]/[.25] bg-[#14181E]/[.25] min-h-[11.5rem] min-w-[39.5rem] flex-[1] flex flex-col items-center justify-center p-5 border-2 rounded-sm border-[#eeeeee] border-dashed outline-none hover:border-[#2196f3] duration-[24ms] transition" {...getRootProps()}> + <input {...getInputProps()} /> + <p className="text-base text-gray-900 dark:text-gray-200">Drag &apos;n&apos; drop some images here, or click to select images</p> + </div> + </section> + )} + </Dropzone> + </div> + </main> + ); + } +} +\ No newline at end of file diff --git a/package-lock.json b/package-lock.json @@ -14,6 +14,7 @@ "next": "12.0.8", "node-localstorage": "2.2.1", "pouchdb": "7.2.2", + "prop-types": "^15.8.1", "react": "17.0.2", "react-blurhash": "0.1.3", "react-dom": "17.0.2", diff --git a/package.json b/package.json @@ -25,6 +25,7 @@ "next": "12.0.8", "node-localstorage": "2.2.1", "pouchdb": "7.2.2", + "prop-types": "15.8.1", "react": "17.0.2", "react-blurhash": "0.1.3", "react-dom": "17.0.2", diff --git a/pages/index.tsx b/pages/index.tsx @@ -95,7 +95,7 @@ class Home extends PureComponent<Props, State>{ </Head> <Header></Header> <main className='mb-auto lg:pt-20 pt-52 z-0'> - <div className='z-[100] sticky lg:top-20 top-[12.5rem] bg-[#fefefe]/[.95] dark:bg-[#14181E]/[.95]'> + <div className='z-[100] sticky lg:top-20 top-[12.5rem] bg-[#fefefe]/[.95] dark:bg-[#12161D]'> <div className='h-16 px-10 w-full relative grid grid-cols-[1fr_auto_1fr] items-center' id='section-grid'> <h1 className='text-xl text-gray-900 dark:text-gray-200 font-bold'>Home</h1> </div> diff --git a/pages/login.tsx b/pages/login.tsx @@ -28,11 +28,11 @@ class Login extends PureComponent<Props, State> { this.state = { showServerField: false, generateProfile: false, - mxid: undefined, - password: undefined, + mxid: "", + password: "", serverUrl: constMatrixArtServer, loading: false, - error: undefined + error: "" } as State; this.handleInputChange = this.handleInputChange.bind(this); diff --git a/pages/submit.tsx b/pages/submit.tsx @@ -1,9 +1,11 @@ import Head from "next/head"; import { PureComponent, ReactNode } from "react"; -import Dropzone, { FileRejection } from "react-dropzone"; +import { FileRejection } from "react-dropzone"; import { ClientContext } from "../components/ClientContext"; import Footer from "../components/Footer"; import Header from "../components/Header"; +import MainSubmissionForm from "../components/submit_page/main"; +import { DropCallbacks, StartSubmit } from "../components/submit_page/start"; type Props = {}; @@ -11,14 +13,15 @@ type State = { files: PreviewWithDataFile[]; fileRejections: FileRejection[]; error?: string; + submitState: "start" | "editing"; }; -type PreviewWithDataFile = File & { - preview: HTMLImageElement; +export type PreviewWithDataFile = File & { + preview_url: string; data: ArrayBuffer; }; -class Submit extends PureComponent<Props, State> { +class Submit extends PureComponent<Props, State> implements DropCallbacks { declare context: React.ContextType<typeof ClientContext>; onDrop: (files: File[]) => void; @@ -26,36 +29,32 @@ class Submit extends PureComponent<Props, State> { super(props); this.onDrop = async (files: File[]) => { const files_mapped = await Promise.all(files.map(async file => Object.assign(file, { - preview: await this.readFileToSizes(file), + preview_url: URL.createObjectURL(file), data: await this.readFileToArray(file), }))) as PreviewWithDataFile[]; + let newState: "start" | "editing"; + switch (this.state.submitState) { + case "start": { + newState = "editing"; + break; + } + default: { + newState = this.state.submitState; + break; + } + } this.setState({ + submitState: newState, files: [...this.state.files, ...files_mapped] }); }; this.state = { files: [], - fileRejections: [] + fileRejections: [], + submitState: "start" }; } - readFileToSizes(file: File): Promise<HTMLImageElement> { - return new Promise((resolve, reject) => { - var fr = new FileReader(); - fr.addEventListener("load", () => { - const img = new Image(); - // Save as we use readAsDataURL - img.src = fr.result as string; - - resolve(img); - }); - fr.addEventListener("error", (error) => { - reject(error); - }); - fr.readAsDataURL(file); - }); - } - readFileToArray(file: File): Promise<ArrayBuffer> { return new Promise((resolve, reject) => { var fr = new FileReader(); @@ -87,17 +86,11 @@ class Submit extends PureComponent<Props, State> { this.setState({ fileRejections: newRejections }); } - renderThumbs() { - return this.state.files.map(file => ( - <div className="relative cursor-pointer border-2 border-solid border-[#eeeeea] p-1" style={{ height: "212px" }} key={file.name}> - <img alt={file.name} className="h-auto w-full object-cover max-w-full align-bottom" style={{ height: "200px" }} src={file.preview.src} width={file.preview.width} height={file.preview.height} /> - </div> - )); - } + render(): ReactNode { return ( - <div className='min-h-full flex flex-col justify-between bg-[#f8f8f8] dark:bg-[#06070D]'> + <div className='min-h-full flex flex-col bg-[#f8f8f8] dark:bg-[#06070D]'> <Head> <title key="title">Matrix Art | Submit Post</title> <meta property="og:title" content="Matrix Art | Submit Post" key="og-title" /> @@ -106,35 +99,29 @@ class Submit extends PureComponent<Props, State> { <meta property="og:type" content="website" key="og-type" /> </Head> <Header></Header> - <div className='z-[100] sticky lg:top-20 top-[12.5rem] bg-[#fefefe]/[.95] dark:bg-[#14181E]/[.95]'> + <div className='z-[100] sticky lg:top-[4.9rem] top-[12.5rem] bg-[#fefefe]/[.95] dark:bg-[#12161D]'> <div className='h-16 px-10 w-full relative grid grid-cols-[1fr_auto_1fr] items-center' id='section-grid'> <h1 className='text-xl text-gray-900 dark:text-gray-200 font-bold'>Submit Image</h1> </div> </div> - <main className='min-h-full lg:pt-20 pt-52 z-0 bottom-0 relative'> - <div className="min-h-full flex justify-center items-center"> - <div className="flex justify-center items-center mt-4"> - <Dropzone accept='image/*' onDropAccepted={this.onDrop} onDropRejected={this.onDropError}> - {({ getRootProps, getInputProps }) => ( - <section className="flex flex-col"> - <div className="dark:bg-[#fefefe]/[.25] bg-[#14181E]/[.25] min-h-[11.5rem] min-w-[39.5rem] flex-[1] flex flex-col items-center justify-center p-5 border-2 rounded-sm border-[#eeeeee] border-dashed outline-none hover:border-[#2196f3] duration-[24ms] transition" {...getRootProps()}> - <input {...getInputProps()} /> - <p className="text-base text-gray-900 dark:text-gray-200">Drag &apos;n&apos; drop some images here, or click to select images</p> - </div> - <ul className="flex flex-row flex-wrap gap-1 mt-4 max-w-[50rem] justify-evenly"> - {this.renderThumbs()} - <li className='grow-[10]'></li> - </ul> - </section> - )} - </Dropzone> - </div> - </div> - </main> + {this.renderStage()} + <div className="flex-grow"></div> <Footer></Footer> </div> ); } + renderStage(): ReactNode { + switch (this.state.submitState) { + case "start": { + return <StartSubmit onDrop={this.onDrop} onDropError={this.onDropError} />; + } + case "editing": + return <MainSubmissionForm onDrop={this.onDrop} onDropError={this.onDropError} files={this.state.files} />; + default: { + return <></>; + } + } + } } Submit.contextType = ClientContext; diff --git a/styles/globals.css b/styles/globals.css @@ -126,4 +126,9 @@ html, body, #__next { .gap-1 > * { margin: 0.25rem; } +} + + +option[value=""][disabled] { + display: none; } \ No newline at end of file