initial commit

This commit is contained in:
Lucas Jensen
2024-05-01 09:19:01 -07:00
commit 5d67c0c2b2
117 changed files with 9917 additions and 0 deletions

View 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;

View 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;

View 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);
}

View 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;

View File

@@ -0,0 +1,5 @@
.event-form-container {
border: 2px solid var(--group-info-color);
border-radius: 5px;
margin-top: 5px;
}

View 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;

View File

@@ -0,0 +1,8 @@
.headshot-preview {
max-width: 400px;
max-height: 400px;
}
#headshot-upload {
display: block;
}

View 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;

View 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;