Compare commits
10 Commits
c482f6758c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63f50dd1c7 | ||
|
|
77003707ab | ||
|
|
3e27343a30 | ||
|
|
85e77c5cc7 | ||
|
|
0e150e72cc | ||
|
|
8c208adf4a | ||
|
|
4b41a76eb0 | ||
|
|
842709884e | ||
|
|
a961118f77 | ||
|
|
fb305e5509 |
@@ -1,8 +1,6 @@
|
||||
name: react client build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
name: fastapi build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -6,5 +6,4 @@
|
||||
#musicians {
|
||||
padding-top: 70px;
|
||||
margin-top: -70px;
|
||||
margin-bottom: 400px;
|
||||
}
|
||||
|
||||
@@ -15,5 +15,4 @@ hr {
|
||||
#events {
|
||||
padding-top: 70px;
|
||||
margin-top: -70px;
|
||||
margin-bottom: 400px;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user