mirror of
https://github.com/Steffo99/festa.git
synced 2024-12-22 14:44:21 +00:00
WIP: file state things
This commit is contained in:
parent
63efa5b896
commit
dafd64b4c9
11 changed files with 164 additions and 164 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -11,5 +11,6 @@
|
||||||
"node_modules": true,
|
"node_modules": true,
|
||||||
".next": true,
|
".next": true,
|
||||||
"yarn.lock": true,
|
"yarn.lock": true,
|
||||||
}
|
},
|
||||||
|
"editor.formatOnSave": true
|
||||||
}
|
}
|
15
components/editable/Editable.tsx
Normal file
15
components/editable/Editable.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { useDefinedContext } from "../../utils/definedContext";
|
||||||
|
import { EditingContext } from "./EditingContext";
|
||||||
|
|
||||||
|
type EditableProps = {
|
||||||
|
editing: JSX.Element,
|
||||||
|
preview: JSX.Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function Editable({editing, preview}: EditableProps) {
|
||||||
|
const [isEditing,] = useDefinedContext(EditingContext)
|
||||||
|
|
||||||
|
return isEditing ? editing : preview
|
||||||
|
}
|
|
@ -1,72 +0,0 @@
|
||||||
import { faChevronRight, faClock } from "@fortawesome/free-solid-svg-icons"
|
|
||||||
import { HTMLProps } from "react"
|
|
||||||
import { EditingContext } from "./EditingContext"
|
|
||||||
import { useDefinedContext } from "../../utils/definedContext"
|
|
||||||
import { FestaIcon } from "../extensions/FestaIcon"
|
|
||||||
import { FormDateRange } from "../form/FormDateRange"
|
|
||||||
import { HumanDate } from "../HumanDate"
|
|
||||||
|
|
||||||
|
|
||||||
type EditableDateRangeProps = {
|
|
||||||
startProps: HTMLProps<HTMLInputElement> & {value?: string},
|
|
||||||
endProps: HTMLProps<HTMLInputElement> & {value?: string},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function EditableDateRange(props: EditableDateRangeProps) {
|
|
||||||
const [editing,] = useDefinedContext(EditingContext)
|
|
||||||
|
|
||||||
if(editing) {
|
|
||||||
return (
|
|
||||||
<FormDateRange
|
|
||||||
preview={false}
|
|
||||||
icon={
|
|
||||||
<FestaIcon icon={faClock}/>
|
|
||||||
}
|
|
||||||
start={
|
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
{...props.startProps}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
connector={
|
|
||||||
<FestaIcon icon={faChevronRight}/>
|
|
||||||
}
|
|
||||||
end={
|
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
{...props.endProps}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTime = Date.parse(props.startProps.value!)
|
|
||||||
const endTime = Date.parse(props.endProps.value!)
|
|
||||||
|
|
||||||
if(Number.isNaN(startTime) && Number.isNaN(endTime)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const startDate = new Date(startTime)
|
|
||||||
const endDate = new Date(endTime)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormDateRange
|
|
||||||
preview={true}
|
|
||||||
icon={
|
|
||||||
<FestaIcon icon={faClock}/>
|
|
||||||
}
|
|
||||||
start={
|
|
||||||
<HumanDate date={startDate}/>
|
|
||||||
}
|
|
||||||
connector={
|
|
||||||
<FestaIcon icon={faChevronRight}/>
|
|
||||||
}
|
|
||||||
end={
|
|
||||||
<HumanDate date={endDate}/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
16
components/editable/EditableDateTimeLocal.tsx
Normal file
16
components/editable/EditableDateTimeLocal.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { HTMLProps } from "react";
|
||||||
|
import { HumanDate } from "../HumanDate";
|
||||||
|
import { Editable } from "./Editable";
|
||||||
|
|
||||||
|
export function EditableDateTimeLocal(props: HTMLProps<HTMLInputElement> & { value: Date }) {
|
||||||
|
return (
|
||||||
|
<Editable
|
||||||
|
editing={
|
||||||
|
<input type="datetime-local" {...props} value={props.value.toISOString()} />
|
||||||
|
}
|
||||||
|
preview={
|
||||||
|
<HumanDate date={props.value} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
19
components/editable/EditableFile.tsx
Normal file
19
components/editable/EditableFile.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { HTMLProps } from "react";
|
||||||
|
import { Editable } from "./Editable";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controlled input component which displays an `input[type="file"]` in editing mode, and is invisible in preview mode.
|
||||||
|
*
|
||||||
|
* Value is the file's [fakepath](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#value), as a string.
|
||||||
|
*/
|
||||||
|
export function EditableFile(props: HTMLProps<HTMLInputElement> & { value: string }) {
|
||||||
|
return (
|
||||||
|
<Editable
|
||||||
|
editing={
|
||||||
|
<input type="file" {...props} />
|
||||||
|
}
|
||||||
|
preview={<></>}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,17 +1,19 @@
|
||||||
import { HTMLProps } from "react";
|
import { HTMLProps } from "react";
|
||||||
import { EditingContext } from "./EditingContext";
|
|
||||||
import { useDefinedContext } from "../../utils/definedContext";
|
|
||||||
import { FestaMarkdown } from "../extensions/FestaMarkdown";
|
import { FestaMarkdown } from "../extensions/FestaMarkdown";
|
||||||
|
import { Editable } from "./Editable";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controlled input component which displays a `textarea` in editing mode, and renders the input in Markdown using {@link FestaMarkdown} in preview mode.
|
* Controlled input component which displays a `textarea` in editing mode, and renders the input in Markdown using {@link FestaMarkdown} in preview mode.
|
||||||
*/
|
*/
|
||||||
export function EditableMarkdown({value, ...props}: HTMLProps<HTMLTextAreaElement>) {
|
export function EditableMarkdown(props: HTMLProps<HTMLTextAreaElement> & { value: string }) {
|
||||||
const [editing,] = useDefinedContext(EditingContext)
|
return (
|
||||||
|
<Editable
|
||||||
return editing ? (
|
editing={
|
||||||
<textarea value={value} {...props}/>
|
<textarea {...props} />
|
||||||
) : (
|
}
|
||||||
<FestaMarkdown markdown={value as string}/>
|
preview={
|
||||||
|
<FestaMarkdown markdown={props.value} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
import { HTMLProps } from "react";
|
|
||||||
import { EditingContext } from "./EditingContext";
|
|
||||||
import { useDefinedContext } from "../../utils/definedContext";
|
|
||||||
import { Postcard } from "../postcard/Postcard";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controlled input component which displays an `input[type="file"]` in editing mode, and is invisible in preview mode.
|
|
||||||
*
|
|
||||||
* As a side effect, it changes the {@link Postcard} to the input file when it's rendered.
|
|
||||||
*/
|
|
||||||
export function EditablePostcard({value, ...props}: HTMLProps<HTMLInputElement> & {value: string | undefined}) {
|
|
||||||
const [editing,] = useDefinedContext(EditingContext)
|
|
||||||
|
|
||||||
return <>
|
|
||||||
{editing ?
|
|
||||||
<input type="file" value={undefined} {...props}/>
|
|
||||||
: null}
|
|
||||||
<Postcard src={value}/>
|
|
||||||
</>
|
|
||||||
}
|
|
|
@ -1,17 +1,19 @@
|
||||||
import { HTMLProps } from "react";
|
import { HTMLProps } from "react";
|
||||||
import { EditingContext } from "./EditingContext";
|
import { Editable } from "./Editable";
|
||||||
import { useDefinedContext } from "../../utils/definedContext";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controlled input component which displays an `input[type="text"]` in editing mode, and a `span` displaying the input in preview mode.
|
* Controlled input component which displays an `input[type="text"]` in editing mode, and a `span` displaying the input in preview mode.
|
||||||
*/
|
*/
|
||||||
export function EditableText(props: HTMLProps<HTMLInputElement> & {value: string}) {
|
export function EditableText(props: HTMLProps<HTMLInputElement> & { value: string }) {
|
||||||
const [editing,] = useDefinedContext(EditingContext)
|
return (
|
||||||
|
<Editable
|
||||||
return editing ? (
|
editing={
|
||||||
<input type="text" {...props}/>
|
<input type="text" {...props} />
|
||||||
) : (
|
}
|
||||||
<span>{props.value}</span>
|
preview={
|
||||||
|
<span>{props.value}</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -7,13 +7,16 @@ type PostcardProps = {
|
||||||
src?: string | StaticImageData
|
src?: string | StaticImageData
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Postcard({src}: PostcardProps) {
|
export function Postcard({ src }: PostcardProps) {
|
||||||
const {setPostcard} = useDefinedContext(PostcardContext)
|
const { setPostcard } = useDefinedContext(PostcardContext)
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if(src) {
|
if (src) {
|
||||||
if(typeof src === "object") {
|
if (src === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof src === "object") {
|
||||||
setPostcard(src.src)
|
setPostcard(src.src)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
58
hooks/useFileState.ts
Normal file
58
hooks/useFileState.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { useReducer } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
type FileState = {
|
||||||
|
value: string,
|
||||||
|
file: File,
|
||||||
|
url: URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileActionClear = { type: "clear" }
|
||||||
|
type FileActionSet = { type: "set", value: string, file: File }
|
||||||
|
|
||||||
|
type FileAction = FileActionClear | FileActionSet
|
||||||
|
|
||||||
|
|
||||||
|
function fileReducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case "clear":
|
||||||
|
|
||||||
|
case "set":
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function useFileState() {
|
||||||
|
const [value, dispatch] = useReducer(
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
const [value, setValue] = useState<string>("")
|
||||||
|
const [file, setFile] = useState<File | null>(null)
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setValue(e.target.value)
|
||||||
|
const file = e.target.files![0]
|
||||||
|
setFile(file ?? null)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const doClear = useCallback(
|
||||||
|
() => {
|
||||||
|
setValue("")
|
||||||
|
setFile(null)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
file,
|
||||||
|
onChange,
|
||||||
|
doClear,
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
|
@ -10,25 +10,28 @@ import { EditableText } from "../../components/editable/EditableText";
|
||||||
import { ToolToggleEditing } from "../../components/tools/ToolToggleEditing";
|
import { ToolToggleEditing } from "../../components/tools/ToolToggleEditing";
|
||||||
import { EditingContext } from "../../components/editable/EditingContext";
|
import { EditingContext } from "../../components/editable/EditingContext";
|
||||||
import { database } from "../../utils/prismaClient";
|
import { database } from "../../utils/prismaClient";
|
||||||
import { EditablePostcard } from "../../components/editable/EditablePostcard";
|
import { EditableFile } from "../../components/editable/EditableFile";
|
||||||
import { ViewEvent } from "../../components/view/ViewEvent";
|
import { ViewEvent } from "../../components/view/ViewEvent";
|
||||||
import { ToolToggleVisible } from "../../components/tools/ToolToggleVisible";
|
import { ToolToggleVisible } from "../../components/tools/ToolToggleVisible";
|
||||||
import { EditableDateRange } from "../../components/editable/EditableDateRange";
|
import { EditableDateRange } from "../../components/editable/EditableDateRange";
|
||||||
import { WorkInProgress } from "../../components/WorkInProgress";
|
import { WorkInProgress } from "../../components/WorkInProgress";
|
||||||
|
import { FormDateRange } from "../../components/form/FormDateRange";
|
||||||
|
import { Postcard } from "../../components/postcard/Postcard";
|
||||||
|
import { useFileState } from "../../hooks/useFileState";
|
||||||
|
|
||||||
|
|
||||||
export async function getServerSideProps(context: NextPageContext) {
|
export async function getServerSideProps(context: NextPageContext) {
|
||||||
const slug = context.query.slug as string
|
const slug = context.query.slug as string
|
||||||
if(typeof slug === "object") {
|
if (typeof slug === "object") {
|
||||||
return {notFound: true}
|
return { notFound: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = await database.event.findUnique({
|
const event = await database.event.findUnique({
|
||||||
where: {slug},
|
where: { slug },
|
||||||
include: {creator: true}
|
include: { creator: true }
|
||||||
})
|
})
|
||||||
if(!event) {
|
if (!event) {
|
||||||
return {notFound: true}
|
return { notFound: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -41,79 +44,53 @@ export async function getServerSideProps(context: NextPageContext) {
|
||||||
|
|
||||||
|
|
||||||
type PageEventDetailProps = {
|
type PageEventDetailProps = {
|
||||||
event: Event & {creator: User}
|
event: Event & { creator: User }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function PageEventDetail({event}: PageEventDetailProps) {
|
export default function PageEventDetail({ event }: PageEventDetailProps) {
|
||||||
const {t} = useTranslation()
|
const { t } = useTranslation()
|
||||||
const editState = useState<boolean>(false)
|
const editState = useState<boolean>(false)
|
||||||
const [title, setTitle] = useState<string>(event.name)
|
const [title, setTitle] = useState<string>(event.name)
|
||||||
const [description, setDescription] = useState<string>(event.description)
|
const [description, setDescription] = useState<string>(event.description)
|
||||||
const [postcard, setPostcard] = useState<string | null>(event.postcard)
|
const postcard = useFileState()
|
||||||
const [startingAt, setStartingAt] = useState<string>(event.startingAt?.toISOString() ?? "")
|
const [startingAt, setStartingAt] = useState<string>(event.startingAt?.toISOString() ?? "")
|
||||||
const [endingAt, setEndingAt] = useState<string>(event.endingAt?.toISOString() ?? "")
|
const [endingAt, setEndingAt] = useState<string>(event.endingAt?.toISOString() ?? "")
|
||||||
|
|
||||||
const setPostcardBlob = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files![0]
|
|
||||||
|
|
||||||
if(!file) {
|
|
||||||
setPostcard(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const blobUrl = URL.createObjectURL(file)
|
|
||||||
setPostcard(blobUrl)
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Head>
|
<Head>
|
||||||
<title key="title">{event.name} - {t("siteTitle")}</title>
|
<title key="title">{event.name} - {t("siteTitle")}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<WorkInProgress/>
|
<WorkInProgress />
|
||||||
|
<Postcard src={postcard.file ? URL.createObjectURL(postcard.file) : event.postcard ?? undefined} />
|
||||||
<EditingContext.Provider value={editState}>
|
<EditingContext.Provider value={editState}>
|
||||||
<ToolBar vertical="vadapt" horizontal="right">
|
<ToolBar vertical="vadapt" horizontal="right">
|
||||||
<ToolToggleEditing/>
|
<ToolToggleEditing />
|
||||||
<ToolToggleVisible/>
|
<ToolToggleVisible />
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
<ViewEvent
|
<ViewEvent
|
||||||
title={
|
title={
|
||||||
<EditableText
|
<EditableText
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setTitle(e.target.value)}
|
onChange={(e: ChangeEvent<HTMLInputElement>) => setTitle(e.target.value)}
|
||||||
placeholder={t("eventDetailsTitlePlaceholder")}
|
placeholder={t("eventDetailsTitlePlaceholder")}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
postcard={
|
postcard={
|
||||||
<EditablePostcard
|
<EditableFile
|
||||||
value={postcard ?? undefined}
|
value={postcard.value}
|
||||||
onChange={setPostcardBlob}
|
onChange={postcard.onChange}
|
||||||
placeholder={t("eventDetailsPostcardPlaceholder")}
|
placeholder={t("eventDetailsPostcardPlaceholder")}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
description={<>
|
description={<>
|
||||||
<EditableMarkdown
|
<EditableMarkdown
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => setDescription(e.target.value)}
|
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => setDescription(e.target.value)}
|
||||||
rows={3}
|
|
||||||
placeholder={t("eventDetailsDescriptionPlaceholder")}
|
placeholder={t("eventDetailsDescriptionPlaceholder")}
|
||||||
/>
|
/>
|
||||||
</>}
|
</>}
|
||||||
daterange={
|
daterange={<></>}
|
||||||
<EditableDateRange
|
|
||||||
startProps={{
|
|
||||||
value: startingAt ?? "",
|
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => setStartingAt(e.target.value)
|
|
||||||
}}
|
|
||||||
endProps={{
|
|
||||||
value: endingAt ?? "",
|
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => setEndingAt(e.target.value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</EditingContext.Provider>
|
</EditingContext.Provider>
|
||||||
</>
|
</>
|
||||||
|
|
Loading…
Reference in a new issue