diff --git a/Components/AlertProvider.js b/Components/AlertProvider.js index 819933e..654aceb 100644 --- a/Components/AlertProvider.js +++ b/Components/AlertProvider.js @@ -28,8 +28,6 @@ export function AlertProvider(props) { const handleContinueClick = () => { setOpen(false) - console.debug('[handleContinueClick] message: ' + message) - console.info('[handleContinueClick] onContinue: ' + onContinue) onContinue() } diff --git a/Components/CompetitionNavbar.tsx b/Components/CompetitionNavbar.tsx new file mode 100644 index 0000000..6c8c8bc --- /dev/null +++ b/Components/CompetitionNavbar.tsx @@ -0,0 +1,103 @@ +import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap' +import classnames from 'classnames' +import Link from 'next/link' +import { useState } from 'react' +import AuthenticatedCompetition from './authenticated/Competition' +import { useConvexAuth } from 'convex/react' + +export default function CompetitionNavbar(props: any) { + const [activeTab, setActiveTab] = useState(props.tabId) + + const { isAuthenticated } = useConvexAuth() + + const toggle = (tab: number) => { + if (activeTab !== tab) setActiveTab(tab) + } + return ( +
+ {isAuthenticated ? : null} + + + {props.children} + +
+ ) +} diff --git a/Components/Teams.tsx b/Components/Teams.tsx index e62c0a1..1bf466a 100644 --- a/Components/Teams.tsx +++ b/Components/Teams.tsx @@ -1,8 +1,20 @@ -import { Button, Col, List, ListInlineItem, Row } from 'reactstrap' +import { + Button, + Col, + Input, + Label, + List, + ListInlineItem, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + Row, +} from 'reactstrap' import { useMutation, useQuery } from 'convex/react' import { api } from '../convex/_generated/api' import { useState } from 'react' -import InvitesAndJoinRequests from './authenticated/InvitesAndJoinRequests' +import InvitesAndJoinRequests from './authenticated/UserInvites' import { Id } from '../convex/_generated/dataModel' export default function Teams(props: any) { @@ -14,15 +26,32 @@ export default function Teams(props: any) { const [inviteButtonMessage, setInviteBtnMsg] = useState('Invite to Team') const handleJoinClick = (id: Id<'teams'>) => { + setRequestJoinModal(false) requestJoin({ id }) setJoinButtonMessage('Join requested!') } + const handleJoinModalOpen = () => { + setRequestJoinModal(true) + } + + const [modal, setModal] = useState(false) + const [requestJoinModal, setRequestJoinModal] = useState(false) + const handleInviteClick = (joinerId: Id<'users'>) => { + setModal(false) invite({ joinerId, competitionId: props.competitionId }) setInviteBtnMsg('Invite sent!') } + const handleInviteModalOpen = () => { + setModal(true) + } + + const toggle = () => setModal(!modal) + + const toggleJoinRequestModal = () => setRequestJoinModal(!requestJoinModal) + return ( @@ -48,12 +77,68 @@ export default function Teams(props: any) { participant && participant.userMembership.team == item._id } - onClick={() => { - handleInviteClick(member._id) - }} + onClick={handleInviteModalOpen} > {inviteButtonMessage} + + + Invite {member.name} + + + + + + + {' '} + + + + + + Request to Join + + + + + + + {' '} + + + ))} @@ -63,9 +148,7 @@ export default function Teams(props: any) { } disabled={!participant} color="primary" - onClick={() => { - handleJoinClick(item._id) - }} + onClick={handleJoinModalOpen} > {joinButtonMessage} diff --git a/Components/User.tsx b/Components/User.tsx index 7e28a1e..c64e7cc 100644 --- a/Components/User.tsx +++ b/Components/User.tsx @@ -4,7 +4,7 @@ import { Doc } from '../convex/_generated/dataModel' export function UserBubble(sender: Doc<'users'>) { return ( {props.name} {participation ? ( <> - - Enter Submission - @@ -53,7 +46,6 @@ export default function AuthenticatedCompetition(props) { > Delete Competition - View Team ) } diff --git a/Components/authenticated/InvitesAndJoinRequests.tsx b/Components/authenticated/InvitesAndJoinRequests.tsx deleted file mode 100644 index 340a429..0000000 --- a/Components/authenticated/InvitesAndJoinRequests.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function InvitesAndJoinRequests() { - return ( - <> -

Invites and Join Requests

- - ) -} diff --git a/Components/authenticated/UserInvites.tsx b/Components/authenticated/UserInvites.tsx new file mode 100644 index 0000000..a32323e --- /dev/null +++ b/Components/authenticated/UserInvites.tsx @@ -0,0 +1,7 @@ +export default function UserInvites() { + return ( + <> +

Invites

+ + ) +} diff --git a/convex/competition.ts b/convex/competition.ts index f2f7a2c..09be5cc 100644 --- a/convex/competition.ts +++ b/convex/competition.ts @@ -56,6 +56,7 @@ export const deleteCompetition = mutation({ args: { id: v.id('competitions') }, handler: async ({ db }, { id }) => { await db.delete(id) + // TODO: New table for submissions attached to a team so these can be separated }, }) diff --git a/convex/lib/team.ts b/convex/lib/team.ts index 0e2b0e4..14cde09 100644 --- a/convex/lib/team.ts +++ b/convex/lib/team.ts @@ -1,6 +1,6 @@ import { GenericDatabaseReader, GenericDatabaseWriter } from 'convex/server' import { DataModel, Doc, Id } from '../_generated/dataModel' -import { RequestValidity } from '../../shared/info' +import { RequestValidity } from '../../lib/shared' import { ConvexError } from 'convex/values' import { convertToUserDocumentArray, fulfillAndFlatten } from './helpers' diff --git a/convex/participant.ts b/convex/participant.ts index ad1df00..45d0760 100644 --- a/convex/participant.ts +++ b/convex/participant.ts @@ -6,7 +6,7 @@ import { addUserToTeam, validateTeamJoinRequest, } from './lib/team' -import { RequestValidity } from '../shared/info' +import { RequestValidity } from '../lib/shared' /** * @inheritDoc team.findTeamOfUser @@ -65,8 +65,11 @@ export const joinCompetition = mutation({ * @param id The id of the team that the user would like to join */ export const requestJoin = mutation({ - args: { id: v.id('teams') }, - handler: async ({ db, auth }, { id }) => { + args: { id: v.id('teams'), pitch: v.optional(v.string()) }, + handler: async ( + { db, auth }, + { id, pitch = 'Hey there! I would like to join your team.' } + ) => { const user = await verifyUser(db, auth) const inviterTeam = await db.get(id) if (!inviterTeam) { @@ -84,6 +87,7 @@ export const requestJoin = mutation({ user: user._id, userConsent: true, teamConsent: false, + pitch, }) return await db.replace(inviterTeam._id, inviterTeam) case RequestValidity.INVITED: @@ -105,8 +109,19 @@ export const requestJoin = mutation({ * @param competitionId The id of the competition that the team is in */ export const inviteToTeam = mutation({ - args: { joinerId: v.id('users'), competitionId: v.id('competitions') }, - handler: async ({ db, auth }, { joinerId, competitionId }) => { + args: { + joinerId: v.id('users'), + competitionId: v.id('competitions'), + pitch: v.optional(v.string()), + }, + handler: async ( + { db, auth }, + { + joinerId, + competitionId, + pitch = 'Hey there! We would love for you to join our team. ', + } + ) => { const inviter = await verifyUser(db, auth) const inviterTeam = await findTeamOfUser(db, inviter, competitionId) if (!inviterTeam) { @@ -129,6 +144,7 @@ export const inviteToTeam = mutation({ user: joiner._id, userConsent: false, teamConsent: true, + pitch, }) return await db.patch(inviterTeam._id, { joinRequests: inviterTeam.joinRequests, diff --git a/convex/schema.ts b/convex/schema.ts index 7751a1b..186307b 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -24,6 +24,7 @@ export const joinRequest = { user: v.id('users'), teamConsent: v.boolean(), userConsent: v.boolean(), + pitch: v.string(), } const team = { diff --git a/convex/team.ts b/convex/team.ts index b7094ae..51c8228 100644 --- a/convex/team.ts +++ b/convex/team.ts @@ -10,7 +10,9 @@ import { includeUserDocument } from './lib/helpers' export const list = query({ args: { competitionId: v.id('competitions') }, handler: async ({ db, auth }, { competitionId }) => { - return await listCompetitionTeams(db, competitionId) + const competitionTeams = await listCompetitionTeams(db, competitionId) + competitionTeams.sort((a, b) => a.members.length - b.members.length) + return competitionTeams }, }) diff --git a/lib/client.ts b/lib/client.ts new file mode 100644 index 0000000..51176c0 --- /dev/null +++ b/lib/client.ts @@ -0,0 +1,47 @@ +import {api} from '../convex/_generated/api' +import {ConvexHttpClient} from 'convex/browser' +import {Id} from '../convex/_generated/dataModel' +import {GetStaticPaths, GetStaticProps, GetStaticPropsContext} from 'next' + +const convex = new ConvexHttpClient( + process.env.NEXT_PUBLIC_CONVEX_DEPLOYMENT_URL || '' +) + +export const Competition = { + routes: async function () { + const competitions = await convex.query(api.competition.listCompetitions) + + return { + paths: competitions.map((item) => { + return { + params: {id: item._id}, + } + }), + fallback: false, + } + } satisfies GetStaticPaths, + page: async function (context: GetStaticPropsContext) { + if (!context.params) { + return { + notFound: true, + } + } + + const competition = await convex.query(api.competition.getCompetition, { + id: context.params.id as Id<'competitions'>, + }) + + if (!competition) { + return { + notFound: true, + } + } + + return { + props: { + id: competition._id, + ...competition, + }, + } + } satisfies GetStaticProps, +} diff --git a/shared/info.ts b/lib/shared.ts similarity index 100% rename from shared/info.ts rename to lib/shared.ts diff --git a/pages/competitions/[id].tsx b/pages/competitions/[id].tsx index 4e8cf2f..99d4786 100644 --- a/pages/competitions/[id].tsx +++ b/pages/competitions/[id].tsx @@ -1,21 +1,7 @@ -import classnames from 'classnames' -import {ConvexHttpClient} from 'convex/browser' import Head from 'next/head' -import {useState} from 'react' -import {Nav, NavItem, NavLink, TabContent, TabPane} from 'reactstrap' -import {useConvexAuth} from 'convex/react' -import {api} from '../../convex/_generated/api' -import Rules from '../../Components/rules' -import SubmissionsList from '../../Components/submissionsList' import Overview from '../../Components/overview' -import AuthenticatedCompetition from '../../Components/authenticated/Competition' -import {GetStaticPaths, GetStaticProps} from "next"; -import {Id} from "../../convex/_generated/dataModel"; -import Teams from "../../Components/Teams"; - -const convex = new ConvexHttpClient( - process.env.NEXT_PUBLIC_CONVEX_DEPLOYMENT_URL || '' -) +import CompetitionNavbar from '../../Components/CompetitionNavbar' +import { Competition } from '../../lib/client' /** * @param props.id {string} The id of the competition @@ -23,128 +9,18 @@ const convex = new ConvexHttpClient( * @param props.thumbnail {string} The thumbnail of the competition */ export default function App(props: any) { - const [activeTab, setActiveTab] = useState(1) - const {isAuthenticated} = useConvexAuth() - - const toggle = (tab: number) => { - if (activeTab !== tab) setActiveTab(tab) - } - - return ( -
- - {props.name} | Musathon - - - {isAuthenticated ? : null} -
- - - - - - - - - {JSON.stringify(props.prizeList)} - - - - - - - -
-
- ) + return ( +
+ + {props.name} | Musathon + + + + + +
+ ) } -export const getStaticProps = (async (context) => { - if (!context.params) { - return { - notFound: true - } - } - - const competition = await convex.query( - api.competition.getCompetition, - {id: context.params.id as Id<'competitions'>} - ) - - if (!competition) { - return { - notFound: true - } - } - - return { - props: { - id: competition._id, ...competition - } - } -}) satisfies GetStaticProps - -export const getStaticPaths = (async () => { - const competitions = await convex.query(api.competition.listCompetitions) - - return { - paths: competitions.map((item) => { - return { - params: {id: item._id}, - } - }), - fallback: false, - } -}) satisfies GetStaticPaths +export const getStaticProps = Competition.page +export const getStaticPaths = Competition.routes diff --git a/pages/competitions/[id]/gallery.tsx b/pages/competitions/[id]/gallery.tsx new file mode 100644 index 0000000..334bd31 --- /dev/null +++ b/pages/competitions/[id]/gallery.tsx @@ -0,0 +1,18 @@ +import CompetitionNavbar from '../../../Components/CompetitionNavbar' +import SubmissionsList from '../../../Components/submissionsList' +import { Competition } from '../../../lib/client' +import Head from 'next/head' + +export default function CompetitionGallery(props: any) { + return ( + + + Submission Gallery + + + + ) +} + +export const getStaticPaths = Competition.routes +export const getStaticProps = Competition.page diff --git a/pages/competitions/[id]/prizes.tsx b/pages/competitions/[id]/prizes.tsx new file mode 100644 index 0000000..5b44873 --- /dev/null +++ b/pages/competitions/[id]/prizes.tsx @@ -0,0 +1,17 @@ +import CompetitionNavbar from '../../../Components/CompetitionNavbar' +import { Competition } from '../../../lib/client' +import Head from 'next/head' + +export default function CompetitionPrizes(props: any) { + return ( + + + Prizes + + {JSON.stringify(props.prizeList)} + + ) +} + +export const getStaticPaths = Competition.routes +export const getStaticProps = Competition.page diff --git a/pages/competitions/[id]/rules.tsx b/pages/competitions/[id]/rules.tsx new file mode 100644 index 0000000..ff36bee --- /dev/null +++ b/pages/competitions/[id]/rules.tsx @@ -0,0 +1,18 @@ +import Rules from '../../../Components/rules' +import CompetitionNavbar from '../../../Components/CompetitionNavbar' +import { Competition } from '../../../lib/client' +import Head from 'next/head' + +export default function CompetitionRules(props: any) { + return ( + + + Competition Rules + + + + ) +} + +export const getStaticPaths = Competition.routes +export const getStaticProps = Competition.page diff --git a/pages/competitions/[id]/team.tsx b/pages/competitions/[id]/team.tsx index af52d13..79fd7e4 100644 --- a/pages/competitions/[id]/team.tsx +++ b/pages/competitions/[id]/team.tsx @@ -1,119 +1,84 @@ -import {Button, Col, Container, List, Row} from 'reactstrap' -import {useConvexAuth, useMutation, useQuery} from 'convex/react' -import {api} from '../../../convex/_generated/api' -import {Id} from "../../../convex/_generated/dataModel"; -import {GetStaticPaths, GetStaticProps} from "next"; -import {ConvexHttpClient} from "convex/browser"; -import React from "react"; -import TeamMembers from "../../../Components/team/TeamMembers"; -import Chat from "../../../Components/Chat"; -import {UserBubble} from "../../../Components/User"; - -const convex = new ConvexHttpClient( - process.env.NEXT_PUBLIC_CONVEX_DEPLOYMENT_URL || '' -) +import { Button, Col, List, Row } from 'reactstrap' +import { useMutation, useQuery } from 'convex/react' +import { api } from '../../../convex/_generated/api' +import { Id } from '../../../convex/_generated/dataModel' +import React from 'react' +import TeamMembers from '../../../Components/team/TeamMembers' +import Chat from '../../../Components/Chat' +import { UserBubble } from '../../../Components/User' +import Head from 'next/head' +import CompetitionNavbar from '../../../Components/CompetitionNavbar' +import { Competition } from '../../../lib/client' +import Link from 'next/link' export default function Team(props: any) { - const teamInfo = useQuery(api.team.get, {competitionId: props.id}) - const acceptJoin = useMutation(api.participant.inviteToTeam) - const {isAuthenticated} = useConvexAuth() - - if (!teamInfo) { - return ( - - You must join the competition to be on a team - - ) - } - - if (!isAuthenticated) { - return ( - - You must be signed in to view your team - - ) - } - - const handleAcceptJoin = (joinerId: Id<'users'>) => { - acceptJoin({ - joinerId, - competitionId: props.id - }) - } + const teamInfo = useQuery(api.team.get, { competitionId: props.id }) + const acceptJoin = useMutation(api.participant.inviteToTeam) - return ( - -

Team Dashboard

- - - - - -

Join Requests

- - {teamInfo.joinRequests.map(request => ( -
  • - - -
  • - ))} -
    -

    Pending Invitations

    - - {teamInfo.invitations.map(invitation => ( -
  • - - {invitation.user.name} -
  • - ))} -
    - -
    - - - - - + const handleAcceptJoin = (joinerId: Id<'users'>) => { + acceptJoin({ + joinerId, + competitionId: props.id, + }) + } - -
    - ) + return ( + + + My Team + +

    Team Dashboard

    + + + {teamInfo ? : null} + + +

    Join Requests

    + + {teamInfo?.joinRequests.map((request) => ( +
  • + +

    {request.pitch}

    + +
  • + ))} +
    +

    Pending Invitations

    + + {teamInfo?.invitations.map((invitation) => ( +
  • + + {invitation.user.name} + +

    {invitation.pitch}

    +
  • + ))} +
    + +
    + + +

    Submission

    + + Enter Submission + + + + {teamInfo ? ( + + ) : null} + +
    +
    + ) } -export const getStaticProps = (async (context) => { - if (!context.params) { - return { - notFound: true - } - } - - const competition = await convex.query( - api.competition.getCompetition, - {id: context.params.id as Id<'competitions'>} - ) - - if (!competition) { - return { - notFound: true - } - } - - return { - props: { - id: competition._id, ...competition - } - } -}) satisfies GetStaticProps - -export const getStaticPaths = (async () => { - const competitions = await convex.query(api.competition.listCompetitions) - // console.log(competitions) - return { - paths: competitions.map((item) => { - return { - params: {id: item._id}, - } - }), - fallback: false, - } -}) satisfies GetStaticPaths +export const getStaticProps = Competition.page +export const getStaticPaths = Competition.routes diff --git a/pages/competitions/[id]/teams.tsx b/pages/competitions/[id]/teams.tsx new file mode 100644 index 0000000..bebe32c --- /dev/null +++ b/pages/competitions/[id]/teams.tsx @@ -0,0 +1,18 @@ +import CompetitionNavbar from '../../../Components/CompetitionNavbar' +import Teams from '../../../Components/Teams' +import { Competition } from '../../../lib/client' +import Head from 'next/head' + +export default function CompetitionTeams(props: any) { + return ( + + + Teams + + + + ) +} + +export const getStaticPaths = Competition.routes +export const getStaticProps = Competition.page diff --git a/pages/new-competition.tsx b/pages/new-competition.tsx index 00eba5b..2a0183d 100644 --- a/pages/new-competition.tsx +++ b/pages/new-competition.tsx @@ -20,9 +20,9 @@ import Head from 'next/head' import { useRouter } from 'next/router' import Link from 'next/link' import { api } from '../convex/_generated/api' -import { Access } from '../shared/info' +import { Access } from '../lib/shared' -export default function App(props: object) { +export default function App() { const router = useRouter() const [user, setUser] = useState('user name') diff --git a/pages/profile.tsx b/pages/profile.tsx index 2a3941a..3a16f13 100644 --- a/pages/profile.tsx +++ b/pages/profile.tsx @@ -14,7 +14,7 @@ import { Label, Row, } from 'reactstrap' -import { useQuery, useMutation, useConvexAuth } from 'convex/react' +import { useQuery, useConvexAuth } from 'convex/react' import { api } from '../convex/_generated/api' function SubmissionViewer() { @@ -57,7 +57,6 @@ export default function Profile() { const { isAuthenticated } = useConvexAuth() const user = useQuery(api.user.getUser) const [bio, setBio] = useState('Loading Bio...') - const storeUser = useMutation(api.user.storeUser) const [editMode, setEditMode] = useState(false) useEffect(() => { @@ -66,10 +65,6 @@ export default function Profile() { } }, [user]) - async function saveBio() { - await storeUser({ bio }) - } - // TODO: Base on NoteFlight profile page: https://www.noteflight.com/profile/e747973656d52d4b41ebe1ff8ebf1cbc8792d7f7 if (!isAuthenticated) { @@ -81,7 +76,7 @@ export default function Profile() {