initial commit
This commit is contained in:
110
client/src/Forms/Bio/BioForm.tsx
Normal file
110
client/src/Forms/Bio/BioForm.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Button, Container, Form } from "react-bootstrap";
|
||||
import { MusicianObj } from "../../Musicians/Musician/Musician";
|
||||
import { GroupObj } from "../../Group/Group";
|
||||
import { patchMusician, patchGroup } from "../../api";
|
||||
import { useState } from "react";
|
||||
|
||||
interface EditBioFormProps {
|
||||
entity: MusicianObj | GroupObj;
|
||||
hideModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onBioChange: () => void;
|
||||
setGroup?: React.Dispatch<React.SetStateAction<GroupObj | undefined>>;
|
||||
setMusician?: React.Dispatch<React.SetStateAction<MusicianObj>>;
|
||||
token: string;
|
||||
}
|
||||
|
||||
function EditBioForm(props: EditBioFormProps) {
|
||||
const [formBio, setFormBio] = useState<string>(props.entity.bio);
|
||||
const [canSubmit, setCanSubmit] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const handleBioChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setFormBio(event.target.value);
|
||||
setCanSubmit(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (props.entity instanceof MusicianObj) {
|
||||
updateMusician(props.token, props.entity);
|
||||
} else if (props.entity instanceof GroupObj) {
|
||||
updateGroup(props.token, props.entity);
|
||||
} else {
|
||||
console.error("Invalid entity type");
|
||||
}
|
||||
props.onBioChange();
|
||||
props.hideModal(false);
|
||||
};
|
||||
|
||||
const updateMusician = async (
|
||||
accessToken: string,
|
||||
musician: MusicianObj,
|
||||
): Promise<void> => {
|
||||
patchMusician(
|
||||
musician.id,
|
||||
formBio,
|
||||
musician.name,
|
||||
musician.headshot_id,
|
||||
accessToken,
|
||||
)
|
||||
.then((patchedMusician) => {
|
||||
if (props.setMusician) {
|
||||
props.setMusician(patchedMusician);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setError("Failed to update bio: " + error.response.data.detail);
|
||||
});
|
||||
};
|
||||
|
||||
const updateGroup = async (
|
||||
accessToken: string,
|
||||
group: GroupObj,
|
||||
): Promise<void> => {
|
||||
patchGroup(group.id, formBio, group.name, accessToken)
|
||||
.then((patchedGroup) => {
|
||||
if (props.setGroup) {
|
||||
props.setGroup(patchedGroup);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setError("Failed to update bio: " + error.response.data.detail);
|
||||
});
|
||||
};
|
||||
|
||||
const SubmitButton = canSubmit ? (
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="primary" type="submit" disabled>
|
||||
Submit
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="formBio">
|
||||
<Form.Label>Bio</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={10}
|
||||
required
|
||||
value={formBio}
|
||||
autoFocus
|
||||
onChange={handleBioChange}
|
||||
/>
|
||||
{error && (
|
||||
<Form.Text className="text-danger error-text">{error}</Form.Text>
|
||||
)}
|
||||
</Form.Group>
|
||||
<Container className="d-flex justify-content-end mt-3">
|
||||
{SubmitButton}
|
||||
</Container>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditBioForm;
|
||||
32
client/src/Forms/Contact/Confirmation/ConfirmationModal.tsx
Normal file
32
client/src/Forms/Contact/Confirmation/ConfirmationModal.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
name: string;
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
function ConfirmationModal(props: ConfirmationModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
size="sm"
|
||||
aria-labelledby="contained-modal-title-vcenter"
|
||||
centered
|
||||
>
|
||||
<Modal.Body className="d-flex flex-column align-items-center justify-content-center">
|
||||
<p>Thank you for your message, {props.name}!</p>
|
||||
<Button
|
||||
className="contact-button"
|
||||
variant="primary"
|
||||
onClick={props.onHide}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmationModal;
|
||||
13
client/src/Forms/Contact/ContactForm.css
Normal file
13
client/src/Forms/Contact/ContactForm.css
Normal file
@@ -0,0 +1,13 @@
|
||||
#contact {
|
||||
padding-top: 70px;
|
||||
margin-top: -70px;
|
||||
margin-bottom: 200px;
|
||||
}
|
||||
|
||||
.contact-title {
|
||||
margin-bottom: 6rem;
|
||||
}
|
||||
|
||||
.contact-text {
|
||||
color: var(--group-info-color);
|
||||
}
|
||||
105
client/src/Forms/Contact/ContactForm.tsx
Normal file
105
client/src/Forms/Contact/ContactForm.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Form from "react-bootstrap/Form";
|
||||
import { postMessage } from "../../api";
|
||||
import { useState } from "react";
|
||||
import { Col, Container, Row } from "react-bootstrap";
|
||||
import "./ContactForm.css";
|
||||
import ConfirmationModal from "./Confirmation/ConfirmationModal";
|
||||
import EmailSignupButton from "../../EmailSignupButton/EmailSignupButton";
|
||||
|
||||
function ContactForm() {
|
||||
const [confirmationModalShow, setConfirmationModalShow] = useState(false);
|
||||
const [form, setForm] = useState<{ [key: string]: string }>({
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
});
|
||||
|
||||
const handleFormChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
setForm({ ...form, [event.target.name]: event.target.value });
|
||||
};
|
||||
|
||||
const handleSubmit = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
postMessage(form.name, form.email, form.message)
|
||||
.then(() => {
|
||||
setConfirmationModalShow(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormReset = () => {
|
||||
setForm({ name: "", email: "", message: "" });
|
||||
setConfirmationModalShow(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container id="contact">
|
||||
<Row className="justify-content-center text-end">
|
||||
<Col xs={12} md={8} lg={6}>
|
||||
<Form
|
||||
className="contact-text"
|
||||
id="contact-form"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<h3 className="display-3 contact-title">Contact Us</h3>
|
||||
<Form.Group className="mb-3" controlId="formBasicName">
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter name"
|
||||
required
|
||||
name="name"
|
||||
value={form.name}
|
||||
onChange={handleFormChange}
|
||||
autoComplete="name"
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3" controlId="formBasicEmail">
|
||||
<Form.Control
|
||||
type="email"
|
||||
placeholder="Enter email"
|
||||
required
|
||||
name="email"
|
||||
value={form.email}
|
||||
onChange={handleFormChange}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="formBasicMessage">
|
||||
<Form.Label>Message</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={3}
|
||||
required
|
||||
name="message"
|
||||
placeholder="Enter message"
|
||||
value={form.message}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
<ConfirmationModal
|
||||
show={confirmationModalShow}
|
||||
onHide={handleFormReset}
|
||||
name={form.name}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="justify-content-center">
|
||||
<Container className="mt-5 text-center">
|
||||
<EmailSignupButton />
|
||||
</Container>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContactForm;
|
||||
5
client/src/Forms/Event/EventForm.css
Normal file
5
client/src/Forms/Event/EventForm.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.event-form-container {
|
||||
border: 2px solid var(--group-info-color);
|
||||
border-radius: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
246
client/src/Forms/Event/EventForm.tsx
Normal file
246
client/src/Forms/Event/EventForm.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Container, Form } from "react-bootstrap";
|
||||
import { postSeries, putSeries } from "../../api";
|
||||
import { EventSeriesObj } from "../../Series/SeriesList";
|
||||
import { EventObj } from "../../Series/Events.tsx/Event/Event";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import "./EventForm.css";
|
||||
|
||||
interface AddEventFormProps {
|
||||
setModalShow: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSeriesList: React.Dispatch<React.SetStateAction<EventSeriesObj[]>>;
|
||||
seriesList: EventSeriesObj[];
|
||||
isNewSeries: boolean;
|
||||
series?: EventSeriesObj;
|
||||
setSeries?: React.Dispatch<React.SetStateAction<EventSeriesObj>>;
|
||||
token: string;
|
||||
}
|
||||
|
||||
function EventForm(props: AddEventFormProps) {
|
||||
/*
|
||||
this form serves two purposes:
|
||||
1. Create a new series
|
||||
2. Edit an existing series
|
||||
|
||||
if isNewSeries is true,an empty form will be rendered to create a new series and post it to the server
|
||||
if isNewSeries is false, the form will be populated with existing series data and will be used to edit the series
|
||||
*/
|
||||
if (props.series && props.isNewSeries) {
|
||||
throw new Error("series provided for new series form");
|
||||
}
|
||||
const [formEvents, setFormEvents] = useState<(EventObj | undefined)[]>(
|
||||
props.series === undefined
|
||||
? [undefined]
|
||||
: props.series.events.map((event) => event),
|
||||
);
|
||||
const [postError, setPostError] = useState<string>("");
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
const form = event.target as HTMLFormElement;
|
||||
const formElements = form.elements as HTMLFormControlsCollection;
|
||||
const seriesNameElement = formElements.namedItem(
|
||||
"formSeriesName",
|
||||
) as HTMLInputElement;
|
||||
|
||||
const seriesName = seriesNameElement.value;
|
||||
const seriesDescriptionElement = formElements.namedItem(
|
||||
"formSeriesDescription",
|
||||
) as HTMLInputElement;
|
||||
const seriesDescription = seriesDescriptionElement.value;
|
||||
const eventObjects: EventObj[] = findEventElements(formElements);
|
||||
|
||||
const series = new EventSeriesObj(
|
||||
(props.series && props.series.series_id) || 0,
|
||||
seriesName,
|
||||
seriesDescription,
|
||||
eventObjects,
|
||||
(props.series && props.series.poster_id) || undefined,
|
||||
);
|
||||
|
||||
if (!props.token) {
|
||||
console.error("no access token");
|
||||
return;
|
||||
}
|
||||
if (props.isNewSeries) {
|
||||
postSeries(series, props.token)
|
||||
.then((newSeries) => {
|
||||
props.setSeriesList([...props.seriesList, newSeries]);
|
||||
props.setModalShow(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setPostError(
|
||||
"Failed to create series: " + error.response.data.detail,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
putSeries(series, props.token)
|
||||
.then((updatedSeries) => {
|
||||
props.setSeries?.(updatedSeries);
|
||||
props.setModalShow(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setPostError(
|
||||
"Failed to update series: " + error.response.data.detail,
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function findEventElements(
|
||||
formElements: HTMLFormControlsCollection,
|
||||
): EventObj[] {
|
||||
const events: EventObj[] = [];
|
||||
for (let idx = 0; idx < formEvents.length; idx++) {
|
||||
const locationElement = formElements.namedItem(
|
||||
`formEvent${idx}Location`,
|
||||
) as HTMLInputElement;
|
||||
const timeElement = formElements.namedItem(
|
||||
`formEvent${idx}Time`,
|
||||
) as HTMLInputElement;
|
||||
const ticketUrlElement = formElements.namedItem(
|
||||
`formEvent${idx}TicketUrl`,
|
||||
) as HTMLInputElement;
|
||||
const mapUrlElement = formElements.namedItem(
|
||||
`formEvent${idx}MapUrl`,
|
||||
) as HTMLInputElement;
|
||||
const location = locationElement.value;
|
||||
const time = timeElement.value;
|
||||
const ticketUrl = ticketUrlElement.value
|
||||
? ticketUrlElement.value
|
||||
: undefined;
|
||||
const mapUrl = mapUrlElement.value ? mapUrlElement.value : undefined;
|
||||
const event = new EventObj(0, location, time, ticketUrl, mapUrl);
|
||||
events.push(event);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
const AddEventButton = (
|
||||
<Container className="d-flex justify-content-end mt-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setFormEvents([...formEvents, undefined])}
|
||||
>
|
||||
Add Event to Series
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const SubmitButton = (
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-3" controlId="formSeriesName">
|
||||
<Form.Label>Series Name *</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter series name"
|
||||
required
|
||||
name="name"
|
||||
{...(props.series && { defaultValue: props.series.name })}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3" controlId="formSeriesDescription">
|
||||
<Form.Label>Series Description *</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={3}
|
||||
placeholder="Enter series description"
|
||||
required
|
||||
name="description"
|
||||
{...(props.series && { defaultValue: props.series.description })}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
{formEvents.map((event, idx) => (
|
||||
<Container
|
||||
key={event ? event.event_id : `temp-${idx}`}
|
||||
className="event-form-container"
|
||||
>
|
||||
<Form.Text className="mb-3">{`Event ${idx}`}</Form.Text>
|
||||
<Form.Group className="mb-3" controlId={`formEvent${idx}Location`}>
|
||||
<Form.Label>Event Location *</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter event location"
|
||||
required
|
||||
name="location"
|
||||
{...(event && {
|
||||
defaultValue: event.location,
|
||||
})}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3" controlId={`formEvent${idx}Time`}>
|
||||
<Form.Label>Event Time *</Form.Label>
|
||||
<Form.Control
|
||||
type="datetime-local"
|
||||
required
|
||||
name="time"
|
||||
{...(event && {
|
||||
defaultValue: event.time,
|
||||
})}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3" controlId={`formEvent${idx}TicketUrl`}>
|
||||
<Form.Label>Event Ticket URL</Form.Label>
|
||||
<Form.Control
|
||||
type="url"
|
||||
name="ticket_url"
|
||||
{...(event && {
|
||||
defaultValue: event.ticket_url,
|
||||
})}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3" controlId={`formEvent${idx}MapUrl`}>
|
||||
<Form.Label>Event Map URL</Form.Label>
|
||||
<Form.Control
|
||||
type="url"
|
||||
name="map_url"
|
||||
{...(event && {
|
||||
defaultValue: event.map_url,
|
||||
})}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Container className="d-flex justify-content-end mb-1">
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
if (event) {
|
||||
setFormEvents(
|
||||
formEvents.filter((e) => e?.event_id !== event.event_id),
|
||||
);
|
||||
} else {
|
||||
setFormEvents(formEvents.filter((_, i) => i !== idx));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</Button>
|
||||
</Container>
|
||||
</Container>
|
||||
))}
|
||||
|
||||
{AddEventButton}
|
||||
|
||||
<Form.Text>
|
||||
A poster can be added after returning to the homepage.
|
||||
</Form.Text>
|
||||
{postError && (
|
||||
<Form.Text className="text-danger error-text">{postError}</Form.Text>
|
||||
)}
|
||||
|
||||
<Container className="d-flex justify-content-end">
|
||||
{SubmitButton}
|
||||
</Container>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventForm;
|
||||
8
client/src/Forms/HeadshotUpload/HeadshotUpload.css
Normal file
8
client/src/Forms/HeadshotUpload/HeadshotUpload.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.headshot-preview {
|
||||
max-width: 400px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
#headshot-upload {
|
||||
display: block;
|
||||
}
|
||||
123
client/src/Forms/HeadshotUpload/HeadshotUploadForm.tsx
Normal file
123
client/src/Forms/HeadshotUpload/HeadshotUploadForm.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Form, Image, Button, Container } from "react-bootstrap";
|
||||
import { MusicianObj } from "../../Musicians/Musician/Musician";
|
||||
import { useState } from "react";
|
||||
import { postHeadshot } from "../../api";
|
||||
import "./HeadshotUpload.css";
|
||||
|
||||
export const sizeLimit = 1000000; // one megabyte
|
||||
|
||||
interface HeadshotUploadProps {
|
||||
currentHeadshot: string;
|
||||
musician: MusicianObj;
|
||||
onHeadshotChange?: () => void;
|
||||
hideModal?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setMusician?: React.Dispatch<React.SetStateAction<MusicianObj>>;
|
||||
token: string;
|
||||
}
|
||||
|
||||
function HeadshotUpload(props: HeadshotUploadProps) {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [fileError, setFileError] = useState<string>("");
|
||||
const [preview, setPreview] = useState<string>(props.currentHeadshot);
|
||||
const [canSubmit, setCanSubmit] = useState<boolean>(false);
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const allowedTypes = ["image/jpeg", "image/png"];
|
||||
const file = event.target.files?.[0];
|
||||
const fileSize = file?.size; // bytes
|
||||
const fileType = file?.type; // MIME type
|
||||
|
||||
if (fileSize && fileSize > sizeLimit) {
|
||||
console.error("file too large");
|
||||
setFileError("file too large");
|
||||
setCanSubmit(false);
|
||||
return;
|
||||
}
|
||||
if (fileType && !allowedTypes.includes(fileType)) {
|
||||
console.error("invalid file type");
|
||||
setFileError("invalid file type");
|
||||
setCanSubmit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file) {
|
||||
setSelectedFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setPreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
setCanSubmit(true);
|
||||
setFileError("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!props.token) {
|
||||
console.error("no access token");
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.musician && selectedFile) {
|
||||
uploadHeadshot(props.token, props.musician, selectedFile);
|
||||
props.hideModal?.(false);
|
||||
|
||||
return;
|
||||
}
|
||||
console.error("no file selected");
|
||||
};
|
||||
|
||||
const uploadHeadshot = async (
|
||||
accessToken: string,
|
||||
musician: MusicianObj,
|
||||
file: File,
|
||||
) => {
|
||||
postHeadshot(musician.id, file, accessToken)
|
||||
.then((updatedMusician) => {
|
||||
props.onHeadshotChange?.();
|
||||
props.setMusician?.(updatedMusician);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const SubmitButton = canSubmit ? (
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="primary" type="submit" disabled>
|
||||
Submit
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="formFile" className="mb-3">
|
||||
<Container className="d-flex justify-content-center">
|
||||
<Image
|
||||
src={preview}
|
||||
className="img-fluid rounded-circle headshot-preview"
|
||||
alt={`${props.musician.name} headshot`}
|
||||
/>
|
||||
</Container>
|
||||
<Form.Label id="headshot-upload">Upload Headshot</Form.Label>
|
||||
<Form.Control type="file" onChange={handleFileChange} />
|
||||
<Form.Text className="text-muted">
|
||||
size limit: {sizeLimit / 1000000} MB
|
||||
</Form.Text>
|
||||
{fileError && (
|
||||
<Form.Text className="text-danger error-text">{fileError}</Form.Text>
|
||||
)}
|
||||
</Form.Group>
|
||||
<Container className="d-flex justify-content-end">
|
||||
{SubmitButton}
|
||||
</Container>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeadshotUpload;
|
||||
117
client/src/Forms/PosterUpload/PosterUploadForm.tsx
Normal file
117
client/src/Forms/PosterUpload/PosterUploadForm.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Button, Container, Form, Image } from "react-bootstrap";
|
||||
import { EventSeriesObj } from "../../Series/SeriesList";
|
||||
import { useState } from "react";
|
||||
import { sizeLimit } from "../HeadshotUpload/HeadshotUploadForm";
|
||||
import { postSeriesPoster } from "../../api";
|
||||
|
||||
interface PosterUploadFormProps {
|
||||
series: EventSeriesObj;
|
||||
currentPoster: string | undefined;
|
||||
setModalShow: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSeries: React.Dispatch<React.SetStateAction<EventSeriesObj>>;
|
||||
token: string;
|
||||
}
|
||||
|
||||
function PosterUploadForm(props: PosterUploadFormProps) {
|
||||
const [preview, setPreview] = useState<string | undefined>(
|
||||
props.currentPoster,
|
||||
);
|
||||
const [fileError, setFileError] = useState<string>("");
|
||||
const [canSubmit, setCanSubmit] = useState<boolean>(false);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const allowedTypes = ["image/jpeg", "image/png"];
|
||||
const file = event.target.files?.[0];
|
||||
const fileSize = file?.size; // bytes
|
||||
const fileType = file?.type; // MIME type
|
||||
|
||||
if (fileSize && fileSize > sizeLimit) {
|
||||
console.error("file too large");
|
||||
setFileError("file too large");
|
||||
setCanSubmit(false);
|
||||
return;
|
||||
}
|
||||
if (fileType && !allowedTypes.includes(fileType)) {
|
||||
console.error("invalid file type");
|
||||
setFileError("invalid file type");
|
||||
setCanSubmit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (file) {
|
||||
setSelectedFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setPreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
setCanSubmit(true);
|
||||
setFileError("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!props.token) {
|
||||
console.error("no access token");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedFile) {
|
||||
postSeriesPoster(props.series.series_id, selectedFile, props.token)
|
||||
.then((updatedEventObj) => {
|
||||
props.setSeries(updatedEventObj);
|
||||
props.setModalShow(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setFileError(
|
||||
"Failed to upload poster: " + error.response.data.detail,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.error("no file selected");
|
||||
};
|
||||
|
||||
const SubmitButton = canSubmit ? (
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="primary" type="submit" disabled>
|
||||
Submit
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="formFile" className="mb-3">
|
||||
<Container className="d-flex justify-content-center">
|
||||
{preview && (
|
||||
<Image
|
||||
src={preview}
|
||||
className="img-fluid rounded-circle poster-preview"
|
||||
alt={`${props.series.name} poster`}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
<Form.Label id="poster-upload">Upload Poster</Form.Label>
|
||||
<Form.Control type="file" onChange={handleFileChange} />
|
||||
<Form.Text className="text-muted">
|
||||
size limit: {sizeLimit / 1000000} MB
|
||||
</Form.Text>
|
||||
{fileError && (
|
||||
<Form.Text className="text-danger error-text">{fileError}</Form.Text>
|
||||
)}
|
||||
</Form.Group>
|
||||
<Container className="d-flex justify-content-end">
|
||||
{SubmitButton}
|
||||
</Container>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default PosterUploadForm;
|
||||
Reference in New Issue
Block a user