basic frontend and backend functionality. Need to test with actual livestream

This commit is contained in:
Lucas Jensen
2025-01-12 16:47:04 -08:00
parent 5b73cecee9
commit c482f6758c
12 changed files with 122 additions and 18 deletions

View File

@@ -10,6 +10,7 @@ interface EditModalProps {
form: JSX.Element; form: JSX.Element;
entity?: MusicianProps | GroupObj | EventSeriesObj; entity?: MusicianProps | GroupObj | EventSeriesObj;
error?: string; error?: string;
livestream_id?: string;
} }
function EditModal(props: EditModalProps) { function EditModal(props: EditModalProps) {

View File

@@ -11,10 +11,12 @@ interface EditBioFormProps {
setGroup?: React.Dispatch<React.SetStateAction<GroupObj | undefined>>; setGroup?: React.Dispatch<React.SetStateAction<GroupObj | undefined>>;
setMusician?: React.Dispatch<React.SetStateAction<MusicianObj>>; setMusician?: React.Dispatch<React.SetStateAction<MusicianObj>>;
token: string; token: string;
livestream_id?: string;
} }
function EditBioForm(props: EditBioFormProps) { function EditBioForm(props: EditBioFormProps) {
const [formBio, setFormBio] = useState<string>(props.entity.bio); const [formBio, setFormBio] = useState<string>(props.entity.bio);
const [formLivestreamId, setFormLivestreamId] = useState<string | undefined>(props.livestream_id)
const [canSubmit, setCanSubmit] = useState<boolean>(false); const [canSubmit, setCanSubmit] = useState<boolean>(false);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
@@ -23,6 +25,11 @@ function EditBioForm(props: EditBioFormProps) {
setCanSubmit(true); setCanSubmit(true);
}; };
const handleLivestreamChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setFormLivestreamId(event.target.value);
setCanSubmit(true);
}
const handleSubmit = async (event: React.FormEvent) => { const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();
if (props.entity instanceof MusicianObj) { if (props.entity instanceof MusicianObj) {
@@ -53,7 +60,6 @@ function EditBioForm(props: EditBioFormProps) {
} }
}) })
.catch((error) => { .catch((error) => {
console.error(error);
setError("Failed to update bio: " + error.response.data.detail); setError("Failed to update bio: " + error.response.data.detail);
}); });
}; };
@@ -62,7 +68,8 @@ function EditBioForm(props: EditBioFormProps) {
accessToken: string, accessToken: string,
group: GroupObj, group: GroupObj,
): Promise<void> => { ): Promise<void> => {
patchGroup(group.id, formBio, group.name, accessToken) const livestream_id = formLivestreamId ? formLivestreamId : "";
patchGroup(group.id, formBio, livestream_id, group.name, accessToken)
.then((patchedGroup) => { .then((patchedGroup) => {
if (props.setGroup) { if (props.setGroup) {
props.setGroup(patchedGroup); props.setGroup(patchedGroup);
@@ -86,6 +93,26 @@ function EditBioForm(props: EditBioFormProps) {
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
{/* need to account for empty string, which is falsy but the field should still show */}
{props.livestream_id != undefined && (
<Form.Group controlId="formLivestreamId">
<p className="text-muted">
A livestream id is the part of a youtube url following "v=".
For example, "ncyl7cTU9k8" but without the quotations.
Don't mess it up. <br></br>
To remove an embedded livestream, just clear this field and submit the form.
</p>
<Form.Label>Livestream ID:</Form.Label>
<Form.Control
as="textarea"
value={formLivestreamId}
rows={1}
onChange={handleLivestreamChange}
placeholder="ncyl7cTU9k8"
/>
</Form.Group>
)}
<Form.Group controlId="formBio"> <Form.Group controlId="formBio">
<Form.Label>Bio</Form.Label> <Form.Label>Bio</Form.Label>
<Form.Control <Form.Control

View File

@@ -5,16 +5,19 @@ import { useState } from "react";
import EditBioForm from "../Forms/Bio/BioForm"; import EditBioForm from "../Forms/Bio/BioForm";
import { faPen } from "@fortawesome/free-solid-svg-icons"; import { faPen } from "@fortawesome/free-solid-svg-icons";
import EditButton from "../Buttons/EditButton/EditButton"; import EditButton from "../Buttons/EditButton/EditButton";
import LivestreamPlayer from "../LivestreamPlayer/LivestreamPlayer";
export class GroupObj { export class GroupObj {
id: number; id: number;
name: string; name: string;
bio: string; bio: string;
livestream_id: string;
constructor(id: number, name: string, bio: string) { constructor(id: number, name: string, bio: string, livestream_id: string) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.bio = bio; this.bio = bio;
this.livestream_id = livestream_id;
} }
} }
@@ -39,7 +42,7 @@ function Group(props: GroupProps) {
<EditButton <EditButton
setModalShow={setModalShow} setModalShow={setModalShow}
faIcon={faPen} faIcon={faPen}
actionName=" Group Bio" actionName=" Group"
/> />
<EditModal <EditModal
show={modalShow} show={modalShow}
@@ -53,6 +56,7 @@ function Group(props: GroupProps) {
onBioChange={props.onBioChange} onBioChange={props.onBioChange}
setGroup={props.setGroup} setGroup={props.setGroup}
token={props.token} token={props.token}
livestream_id={group.livestream_id}
/> />
} }
/> />
@@ -67,6 +71,9 @@ function Group(props: GroupProps) {
<Card.Title> <Card.Title>
<h1>{group.name}</h1> <h1>{group.name}</h1>
</Card.Title> </Card.Title>
{group.livestream_id && (
<LivestreamPlayer livestreamId={group.livestream_id} />
)}
{props.token && EditIcon} {props.token && EditIcon}
<Card.Text className="lead group-bio">{group.bio}</Card.Text> <Card.Text className="lead group-bio">{group.bio}</Card.Text>
</Card.Body> </Card.Body>

View File

@@ -0,0 +1,25 @@
import { Container } from "react-bootstrap";
interface LivestreamPlayerProps {
livestreamId: string;
}
const LivestreamPlayer = (props: LivestreamPlayerProps) => {
const iframeSrc = `https://www.youtube.com/embed/${props.livestreamId}`;
return (
<Container className="d-flex justify-content-center my-3">
<div className="ratio ratio-16x9">
<iframe
src={iframeSrc}
title="YouTube Livestream"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
allowFullScreen
className="rounded border"
></iframe>
</div>
</Container>
);
};
export default LivestreamPlayer;

View File

@@ -38,6 +38,7 @@ export const getRoot = async (): Promise<TheGrapefruitsDuoAPI> => {
response.data.group.id, response.data.group.id,
response.data.group.name, response.data.group.name,
response.data.group.bio, response.data.group.bio,
response.data.group.livestream_id,
), ),
response.data.musicians.map( response.data.musicians.map(
(musician: MusicianObj) => (musician: MusicianObj) =>
@@ -104,7 +105,12 @@ export const postUser = async (token: string): Promise<UserObj> => {
export const getGroup = async (): Promise<GroupObj> => { export const getGroup = async (): Promise<GroupObj> => {
const response = await api.get("/group/"); const response = await api.get("/group/");
return new GroupObj(response.data.id, response.data.name, response.data.bio); return new GroupObj(
response.data.id,
response.data.name,
response.data.bio,
response.data.livestream_id,
);
}; };
export const getMusicians = async (): Promise<MusicianObj[]> => { export const getMusicians = async (): Promise<MusicianObj[]> => {
@@ -143,15 +149,21 @@ export const patchMusician = async (
export const patchGroup = async ( export const patchGroup = async (
id: number, id: number,
bio: string, bio: string,
livestream_id: string,
name: string, name: string,
user_token: string, user_token: string,
): Promise<GroupObj> => { ): Promise<GroupObj> => {
const response = await api.patch( const response = await api.patch(
`/group/`, `/group/`,
{ id, bio, name }, { id, bio, livestream_id, name },
{ headers: { Authorization: `Bearer ${user_token}` } }, { headers: { Authorization: `Bearer ${user_token}` } },
); );
return new GroupObj(response.data.id, response.data.name, response.data.bio); return new GroupObj(
response.data.id,
response.data.name,
response.data.bio,
response.data.livestream_id,
);
}; };
export const postHeadshot = async ( export const postHeadshot = async (

3
server/.gitignore vendored
View File

@@ -177,3 +177,6 @@ pyrightconfig.json
*.log *.log
.vscode/ .vscode/
# mysql dumps
*.dump

View File

@@ -197,16 +197,20 @@ class MainController:
""" """
return self.group_controller.get_group() return self.group_controller.get_group()
async def update_group_bio( async def update_group(
self, bio: str, token: HTTPAuthorizationCredentials self, group: Group, token: HTTPAuthorizationCredentials
) -> Group: ) -> Group:
""" """
Updates the group's bio and returns the updated group object. Updates the group's bio and returns the updated group object.
:param str bio: The new bio for the group
:param HTTPAuthorizationCredentials token: The OAuth token
:return Group: The updated group object which is suitable for a response body
""" """
_, sub = self.oauth_token.email_and_sub(token) _, sub = self.oauth_token.email_and_sub(token)
self.user_controller.get_user_by_sub(sub) self.user_controller.get_user_by_sub(sub)
return self.group_controller.update_group_bio(bio) self.group_controller.update_livestream(group.livestream_id)
return self.group_controller.update_group_bio(group.bio)
async def update_livestream(
self, livestream_id: str, token: HTTPAuthorizationCredentials
) -> Group:
_, sub = self.oauth_token.email_and_sub(token)
self.user_controller.get_user_by_sub(sub)
return self.group_controller.update_livestream(livestream_id)

View File

@@ -57,3 +57,13 @@ class GroupController(BaseController):
detail=f"Error updating group bio: {e}", detail=f"Error updating group bio: {e}",
) )
return self.get_group() return self.get_group()
def update_livestream(self, livestream_id: str) -> Group:
try:
self.group_queries.update_livestream(livestream_id)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating livestram: {e}",
)
return self.get_group()

View File

@@ -34,3 +34,15 @@ class GroupQueries(BaseQueries):
cursor.execute(query, (bio,)) cursor.execute(query, (bio,))
conn.commit() conn.commit()
self.close_cursor_and_conn(cursor, conn) self.close_cursor_and_conn(cursor, conn)
def update_livestream(self, livestream_id: str) -> None:
cursor, conn = self.get_cursor_and_conn()
query = f"""-- sql
UPDATE {self.table} SET livestream_id = %s WHERE id = 1
"""
cursor.execute(query, (livestream_id,))
conn.commit()
self.close_cursor_and_conn(cursor, conn)
def delete_livestream(self) -> None:
self.update_livestream(livestream_id="")

View File

@@ -1,7 +1,9 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional
class Group(BaseModel): class Group(BaseModel):
name: str name: str
bio: str bio: str
id: int | None = None livestream_id: str = ""
id: Optional[int] = None

View File

@@ -24,4 +24,5 @@ async def update_group(
) -> Group: ) -> Group:
"""Updates the group bio, but requires the entire group object to be sent in the request body. """Updates the group bio, but requires the entire group object to be sent in the request body.
Requires authentication.""" Requires authentication."""
return await controller.update_group_bio(group.bio, token) ic(group)
return await controller.update_group(group, token)

View File

@@ -158,7 +158,7 @@ async def test_get_group():
async def test_update_group_bio(): async def test_update_group_bio():
"""Tests the update_group_bio method.""" """Tests the update_group_bio method."""
bio = "A new bio" bio = "A new bio"
await controller.update_group_bio(bio, mock_token) await controller.update_group(bio, mock_token)
MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token) MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token)
MagicMock.assert_called(mock_user_controller.get_user_by_sub) MagicMock.assert_called(mock_user_controller.get_user_by_sub)
MagicMock.assert_called(mock_group_controller.update_group_bio) MagicMock.assert_called(mock_group_controller.update_group_bio)