Compare commits

...

10 Commits

Author SHA1 Message Date
Lucas Jensen
63f50dd1c7 only build on PR (#2)
Reviewed-on: #2
2025-03-15 04:39:31 +00:00
Lucas Jensen
77003707ab update-readme (#1)
Reviewed-on: #1
2025-03-15 04:27:49 +00:00
Lucas Jensen
3e27343a30 Merge pull request #20 from ljensen505/livestream-program
Livestream program
2025-01-18 19:35:34 -08:00
Lucas Jensen
85e77c5cc7 final testing and cleanup 2025-01-18 19:31:08 -08:00
Lucas Jensen
0e150e72cc add functionality for embedded program 2025-01-18 19:10:49 -08:00
Lucas Jensen
8c208adf4a Merge pull request #19 from ljensen505/livestream
Livestream
2025-01-15 18:15:12 -08:00
Lucas Jensen
4b41a76eb0 fix test to reflect changed group passing 2025-01-15 18:13:34 -08:00
Lucas Jensen
842709884e fix type issues and remove logging 2025-01-15 18:07:43 -08:00
Lucas Jensen
a961118f77 let env vars be overwritten 2025-01-15 17:52:14 -08:00
Lucas Jensen
fb305e5509 minor cleanup and tested with actual livestream 2025-01-15 17:50:44 -08:00
16 changed files with 78 additions and 32 deletions

View File

@@ -1,8 +1,6 @@
name: react client build
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

View File

@@ -1,8 +1,6 @@
name: fastapi build
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

View File

@@ -6,8 +6,8 @@ This repo is for Eugene based chamber duo, The Grapefruits Duo. It roughly follo
More info on each part of the project can be found in their respective directories, along with setup instructions. The application is served from a Linode Ubuntu 22.04 instance running NGINX and SSL certificates from Let's Encrypt.
- [Client](https://github.com/ljensen505/TheGrapefruitsDuo/tree/main/client)
- [Server](https://github.com/ljensen505/TheGrapefruitsDuo/tree/main/server)
- [Client](https://gitea.lucasjensen.me/lucasjensen/TheGrapefruitsDuo/src/branch/main/server)
- [Server](https://gitea.lucasjensen.me/lucasjensen/TheGrapefruitsDuo/src/branch/main/client)
## Auth

View File

@@ -1,6 +1,6 @@
# The Grapefruits Duo
Frontend client for Eugene, OR based chamber music duo, The Grapefruits Duo. Publicly available at [thegrapefruitsduo.com](https://thegrapefruitsduo.com/). This client consumes a RESTful API built with FastAPI and publicly available at [api.thegrapefruitsduo.com](https://api.thegrapefruitsduo.com/). Back-end source code available on [GitHub](https://github.com/ljensen505/thegrapefruitsduo-back).
Frontend client for Eugene, OR based chamber music duo, The Grapefruits Duo. Publicly available at [thegrapefruitsduo.com](https://thegrapefruitsduo.com/). This client consumes a RESTful API built with FastAPI and publicly available at [api.thegrapefruitsduo.com](https://api.thegrapefruitsduo.com/). Back-end source code available on [here](https://gitea.lucasjensen.me/lucasjensen/TheGrapefruitsDuo/src/branch/main/server).
## Features
@@ -9,7 +9,8 @@ The customer-facing page for this SPA is relatively simple. It includes sections
### Getting started
```bash
git clone git@github.com:ljensen505/TheGrapefruitsDuo.git
git clone https://gitea.lucasjensen.me/lucasjensen/TheGrapefruitsDuo.git
# or consult Gitea for ssh uri
```
```bash

View File

@@ -30,6 +30,11 @@ body {
}
}
section {
margin-top: 10em;
margin-bottom: 10em;
}
.btn-primary {
background-color: var(--group-info-color);
border-color: var(--group-info-color);

View File

@@ -1,5 +1,5 @@
import Modal from "react-bootstrap/Modal";
import { MusicianProps } from "../Musicians/Musician/Musician";
import { MusicianObj } from "../Musicians/Musician/Musician";
import { GroupObj } from "../Group/Group";
import { EventSeriesObj } from "../Series/SeriesList";
@@ -8,7 +8,7 @@ interface EditModalProps {
show: boolean;
onHide: () => void;
form: JSX.Element;
entity?: MusicianProps | GroupObj | EventSeriesObj;
entity?: MusicianObj | GroupObj | EventSeriesObj;
error?: string;
livestream_id?: string;
}

View File

@@ -16,7 +16,9 @@ interface EditBioFormProps {
function EditBioForm(props: EditBioFormProps) {
const [formBio, setFormBio] = useState<string>(props.entity.bio);
const [formLivestreamId, setFormLivestreamId] = useState<string | undefined>(props.livestream_id)
const [formLivestreamId, setFormLivestreamId] = useState<string | undefined>(
props.livestream_id,
);
const [canSubmit, setCanSubmit] = useState<boolean>(false);
const [error, setError] = useState<string>("");
@@ -25,10 +27,12 @@ function EditBioForm(props: EditBioFormProps) {
setCanSubmit(true);
};
const handleLivestreamChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const handleLivestreamChange = (
event: React.ChangeEvent<HTMLTextAreaElement>,
) => {
setFormLivestreamId(event.target.value);
setCanSubmit(true);
}
};
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
@@ -97,10 +101,12 @@ function EditBioForm(props: EditBioFormProps) {
{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.
A livestream id is part of a youtube url. Either
".../v=[livestream_id]" or ".../live/[livestream_id]". 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
@@ -108,9 +114,8 @@ function EditBioForm(props: EditBioFormProps) {
value={formLivestreamId}
rows={1}
onChange={handleLivestreamChange}
placeholder="ncyl7cTU9k8"
placeholder=""
/>
</Form.Group>
)}
<Form.Group controlId="formBio">

View File

@@ -1,23 +1,32 @@
import { Card, Container } from "react-bootstrap";
import { Card, Container, Image } from "react-bootstrap";
import "./Group.css";
import EditModal from "../EditModals/EditModal";
import { useState } from "react";
import { useEffect, useState } from "react";
import EditBioForm from "../Forms/Bio/BioForm";
import { faPen } from "@fortawesome/free-solid-svg-icons";
import EditButton from "../Buttons/EditButton/EditButton";
import LivestreamPlayer from "../LivestreamPlayer/LivestreamPlayer";
import cld from "../Cld/CloudinaryConfig";
export class GroupObj {
id: number;
name: string;
bio: string;
livestream_id: string;
livestream_program_cld_id?: string;
constructor(id: number, name: string, bio: string, livestream_id: string) {
constructor(
id: number,
name: string,
bio: string,
livestream_id: string,
livestream_program_cld_id?: string,
) {
this.id = id;
this.name = name;
this.bio = bio;
this.livestream_id = livestream_id;
this.livestream_program_cld_id = livestream_program_cld_id;
}
}
@@ -30,10 +39,23 @@ interface GroupProps {
function Group(props: GroupProps) {
const [modalShow, setModalShow] = useState(false);
if (!props.group) {
const [programId, setProgramId] = useState<string | undefined>(undefined);
const [programUrl, setProgramUrl] = useState<string | undefined>(undefined);
const group = props.group;
useEffect(() => {
if (group) {
setProgramId(group.livestream_program_cld_id);
}
if (programId) {
setProgramUrl(cld.image(programId).toURL());
}
}, [group, programId]);
if (!group) {
return null;
}
const group = props.group;
const EditTitle = `Edit ${group.name}'s Bio`;
@@ -65,7 +87,7 @@ function Group(props: GroupProps) {
return (
<section id="about">
<Container className="vh-100 d-flex align-items-center justify-content-center text-center">
<Container className="d-flex align-items-center justify-content-center text-center">
<Card className="group-info">
<Card.Body>
<Card.Title>
@@ -74,6 +96,17 @@ function Group(props: GroupProps) {
{group.livestream_id && (
<LivestreamPlayer livestreamId={group.livestream_id} />
)}
{group.livestream_id && group.livestream_program_cld_id && (
<Container className="d-flex align-items-center justify-content-center flex-column">
<a href={programUrl} target="_blank">
<Image
src={programUrl}
className="img-fluid"
alt="A of the current livestream"
/>
</a>
</Container>
)}
{props.token && EditIcon}
<Card.Text className="lead group-bio">{group.bio}</Card.Text>
</Card.Body>

View File

@@ -5,7 +5,7 @@ interface LivestreamPlayerProps {
}
const LivestreamPlayer = (props: LivestreamPlayerProps) => {
const iframeSrc = `https://www.youtube.com/embed/${props.livestreamId}`;
const iframeSrc = `https://www.youtube.com/embed/${props.livestreamId}?autoplay=1`;
return (
<Container className="d-flex justify-content-center my-3">

View File

@@ -6,5 +6,4 @@
#musicians {
padding-top: 70px;
margin-top: -70px;
margin-bottom: 400px;
}

View File

@@ -15,5 +15,4 @@ hr {
#events {
padding-top: 70px;
margin-top: -70px;
margin-bottom: 400px;
}

View File

@@ -39,6 +39,7 @@ export const getRoot = async (): Promise<TheGrapefruitsDuoAPI> => {
response.data.group.name,
response.data.group.bio,
response.data.group.livestream_id,
response.data.group.livestream_program_cld_id,
),
response.data.musicians.map(
(musician: MusicianObj) =>
@@ -110,6 +111,7 @@ export const getGroup = async (): Promise<GroupObj> => {
response.data.name,
response.data.bio,
response.data.livestream_id,
response.data.livestream_program_cld_id,
);
};
@@ -152,10 +154,11 @@ export const patchGroup = async (
livestream_id: string,
name: string,
user_token: string,
livestream_program_cld_id?: string,
): Promise<GroupObj> => {
const response = await api.patch(
`/group/`,
{ id, bio, livestream_id, name },
{ id, bio, livestream_id, name, livestream_program_cld_id },
{ headers: { Authorization: `Bearer ${user_token}` } },
);
return new GroupObj(
@@ -163,6 +166,7 @@ export const patchGroup = async (
response.data.name,
response.data.bio,
response.data.livestream_id,
response.data.livestream_program_cld_id,
);
};

View File

@@ -15,7 +15,7 @@ def connect_db() -> mysql.connector.MySQLConnection:
Credential values are validated and an exception is raised if any are missing.
"""
load_dotenv()
load_dotenv(override=True)
host = os.getenv("DB_HOST")
user = os.getenv("DB_USER")
password = os.getenv("DB_PASSWORD")

View File

@@ -6,4 +6,7 @@ class Group(BaseModel):
name: str
bio: str
livestream_id: str = ""
livestream_program_cld_id: Optional[str] = (
None # not a FK! references a cloudinary ID
)
id: Optional[int] = None

View File

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

View File

@@ -4,6 +4,7 @@ import pytest
from app.controllers.controller import MainController
from app.models.event import EventSeries, NewEventSeries
from app.models.group import Group
from app.models.musician import Musician
mock_user_controller = MagicMock()
@@ -158,7 +159,8 @@ async def test_get_group():
async def test_update_group_bio():
"""Tests the update_group_bio method."""
bio = "A new bio"
await controller.update_group(bio, mock_token)
group = Group(name="The Grapefruits Duo", bio=bio, livestream_id="")
await controller.update_group(group, 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_group_controller.update_group_bio)