Skip to content

Commit 7d03a51

Browse files
committed
feat (conferences): initial conference management
1 parent c59b6fd commit 7d03a51

35 files changed

+967
-341
lines changed

Diff for: .eslintrc

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,9 @@
22
"extends": ["next", "prettier"],
33
"parser": "@typescript-eslint/parser",
44
"plugins": ["@typescript-eslint"],
5-
"settings": { "react": { "version": "detect" } }
5+
"settings": { "react": { "version": "detect" } },
6+
"rules": {
7+
"no-unused-vars": "off",
8+
"@typescript-eslint/no-unused-vars": ["error"]
9+
}
610
}

Diff for: README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ If you explore the network tab of the front-end, you will see that no data is re
9494
This is because there is no data, you can go to `http://localhost:9695` and start adding rows and/or modifying your database schema. You can also go to `http://localhost:3000/sign-in` and sign in with the following credentials:
9595

9696
```sh
97-
email: admin@admin.com
98-
password: Admin1234!
97+
email: manager@conferenceplatform.com
98+
password: Manager1234!
9999
```
100100

101101
You can start adding speakers and talks to the database via the front-end.

Diff for: nhost/metadata/databases/default/tables/public_speakers.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ select_permissions:
2525
- social
2626
- user_id
2727
filter: {}
28+
allow_aggregations: true
2829
- role: user
2930
permission:
3031
columns:

Diff for: nhost/seeds/default/1_default_user.sql

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
INSERT INTO auth.users (disabled,email_verified,is_anonymous,phone_number_verified,locale,active_mfa_type,avatar_url,default_role,display_name,otp_hash,otp_method_last_used,password_hash,phone_number,ticket,totp_secret,webauthn_current_challenge,created_at,last_seen,otp_hash_expires_at,ticket_expires_at,updated_at,email,new_email,id) VALUES ('false','true','false','false','en',NULL,'https://s.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?r=g&default=blank','user','admin@admin.com',NULL,NULL,'$2a$10$dZt0HwuszYsCwkEJtVyCLuW7a0NvwsFfcHVVaWaYVGITDlBa.ClkK',NULL,NULL,NULL,NULL,'2022-09-28T08:39:33.964102+00:00','2022-09-28T08:39:34.023+00:00','2022-09-28T08:39:33.964102+00:00','2022-10-28T08:39:33.96+00:00','2022-09-28T08:39:34.028562+00:00','admin@admin.com',NULL,'6f089b4e-ae01-478c-a70c-de4f5649ec8f');
1+
INSERT INTO auth.users (disabled,email_verified,is_anonymous,phone_number_verified,locale,active_mfa_type,avatar_url,default_role,display_name,otp_hash,otp_method_last_used,password_hash,phone_number,ticket,totp_secret,webauthn_current_challenge,created_at,last_seen,otp_hash_expires_at,ticket_expires_at,updated_at,email,new_email,id) VALUES ('false','true','false','false','en',NULL,'https://s.gravatar.com/avatar/64e1b8d34f425d19e1ee2ea7236d3028?r=g&default=blank','user','manager@conferenceplatform.com',NULL,NULL,'$2a$10$if5mL8vPQtTNi7DxTql14OAi2H0IBP1J2K8wuA251fDJv7FMJ1eD.',NULL,NULL,NULL,NULL,'2022-09-28T08:39:33.964102+00:00','2022-09-28T08:39:34.023+00:00','2022-09-28T08:39:33.964102+00:00','2022-10-28T08:39:33.96+00:00','2022-09-28T08:39:34.028562+00:00','manager@conferenceplatform.com',NULL,'6f089b4e-ae01-478c-a70c-de4f5649ec8f');

Diff for: package.json

+2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"@nhost/nextjs": "^1.7.0",
1616
"@nhost/react": "^0.12.0",
1717
"@tanstack/react-query": "^4.0.10",
18+
"@types/lodash.debounce": "^4.0.7",
1819
"graphql": "^16.5.0",
20+
"lodash.debounce": "^4.0.8",
1921
"next": "latest",
2022
"react": "^17.0.2",
2123
"react-dom": "^17.0.2",

Diff for: src/components/Header.tsx

+54-37
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { useRouter } from 'next/router';
88
import { twMerge } from 'tailwind-merge';
99

1010
export function Header() {
11-
const { asPath } = useRouter();
11+
const {
12+
asPath,
13+
query: { conferenceSlug },
14+
} = useRouter();
1215
const userEmail = useUserEmail();
1316
const { isLoading, isAuthenticated } = useAuthenticationStatus();
1417
const { signOut } = useSignOut();
@@ -23,43 +26,57 @@ export function Header() {
2326
</a>
2427
</Link>
2528
</div>
26-
<div className="text-list w-52 flex flex-row self-center space-x-2 text-sm font-medium list-none">
27-
<li
28-
className={twMerge(
29-
'hover:text-white py-1 px-2 cursor-pointer',
30-
asPath === '/speakers' && 'text-white',
31-
)}
32-
>
33-
<Link href="speakers">Speakers</Link>
34-
</li>
35-
<li
36-
className={twMerge(
37-
'hover:text-white py-1 px-2 cursor-pointer',
38-
asPath === '/talks' && 'text-white',
39-
)}
40-
>
41-
<Link href="talks">Talks</Link>
42-
</li>
43-
<li
44-
className={twMerge(
45-
'hover:text-white py-1 px-2 cursor-pointer',
46-
asPath === '/about' && 'text-white',
47-
)}
48-
>
49-
<Link href="about">About</Link>
50-
</li>
51-
</div>
29+
30+
{conferenceSlug && (
31+
<nav className="self-center" aria-label="Main navigation">
32+
<ul className="text-list max-w-[208px] w-full items-center grid grid-flow-col gap-2 text-sm font-medium list-none">
33+
<>
34+
<li
35+
className={twMerge(
36+
'hover:text-white px-2 cursor-pointer',
37+
asPath.endsWith('/speakers') && 'text-white',
38+
)}
39+
>
40+
<Link href={`/conferences/${conferenceSlug}/speakers`}>
41+
Speakers
42+
</Link>
43+
</li>
44+
45+
<li
46+
className={twMerge(
47+
'hover:text-white px-2 cursor-pointer',
48+
asPath.endsWith('/talks') && 'text-white',
49+
)}
50+
>
51+
<Link href={`/conferences/${conferenceSlug}/talks`}>
52+
Talks
53+
</Link>
54+
</li>
55+
</>
56+
57+
<li
58+
className={twMerge(
59+
'hover:text-white px-2 cursor-pointer',
60+
asPath.endsWith('/about') && 'text-white',
61+
)}
62+
>
63+
<Link href={`/conferences/${conferenceSlug}/about`}>About</Link>
64+
</li>
65+
</ul>
66+
</nav>
67+
)}
68+
5269
<div className="flex">
5370
{isAuthenticated && userEmail && (
54-
<div className="flex flex-row space-x-4">
55-
<Link href="/dashboard">
56-
<button className="text-list px-2 py-1 text-xs cursor-pointer">
57-
{userEmail}
58-
</button>
71+
<div className="grid items-center grid-flow-col gap-4">
72+
<Link href="/conferences" passHref>
73+
<a className="text-list hover:underline px-2 py-1 text-xs">
74+
Manage Conferences
75+
</a>
5976
</Link>
6077

6178
<button
62-
onClick={() => signOut()}
79+
onClick={signOut}
6380
className="text-list border-list px-2 py-1 text-xs border rounded-md"
6481
>
6582
Sign Out
@@ -68,10 +85,10 @@ export function Header() {
6885
)}
6986

7087
{!isAuthenticated && !isLoading && (
71-
<Link href="/sign-in">
72-
<button className="border text-xs py-1.5 px-2 text-list hover:border-white hover:text-white transition-colors duration-200 border-list rounded-md flex w-full items-center justify-center">
73-
Organizer Dashboard
74-
</button>
88+
<Link href="/sign-in" passHref>
89+
<a className="text-list hover:border-white hover:text-white border-list flex items-center justify-center w-full px-2 py-1 text-xs transition-colors duration-200 border rounded-md">
90+
Sign In as Organizer
91+
</a>
7592
</Link>
7693
)}
7794
</div>

Diff for: src/components/IndexContainer.tsx

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { Header } from '@/components/Header';
2-
import Layout from '@/components/Layout';
3-
import { data } from '@/data/info';
42
import { ReactNode } from 'react';
53

64
interface IndexContainerProps {
@@ -9,11 +7,10 @@ interface IndexContainerProps {
97

108
export const IndexContainer = ({ children }: IndexContainerProps) => {
119
return (
12-
<Layout title={data.pageTitle}>
13-
<div className="bg-header bg-grid h-full text-white">
14-
<Header />
15-
{children ? children : null}
16-
</div>
17-
</Layout>
10+
<div className="bg-header bg-grid h-full text-white">
11+
<Header />
12+
13+
{children ? children : null}
14+
</div>
1815
);
1916
};

Diff for: src/components/Layout.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { ReactNode } from 'react';
21
import Head from 'next/head';
2+
import { ReactNode } from 'react';
33

44
type Props = {
55
children?: ReactNode;
@@ -13,6 +13,7 @@ const Layout = ({ children, title = 'This is the default title' }: Props) => (
1313
<meta charSet="utf-8" />
1414
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
1515
</Head>
16+
1617
{children}
1718
</div>
1819
);

Diff for: src/components/common/Input.tsx

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { DetailedHTMLProps, ForwardedRef, forwardRef, HTMLProps } from 'react';
2+
import { twMerge } from 'tailwind-merge';
3+
4+
export interface InputProps
5+
extends DetailedHTMLProps<HTMLProps<HTMLInputElement>, HTMLInputElement> {
6+
/**
7+
* Error message
8+
*/
9+
error?: string;
10+
}
11+
12+
export const Input = forwardRef(function Input(
13+
{ className, error, ...props }: InputProps,
14+
ref: ForwardedRef<HTMLInputElement>,
15+
) {
16+
return (
17+
<div className="grid grid-cols-3">
18+
<div className="col-span-1">
19+
<label
20+
htmlFor={props.id}
21+
className="text-list self-center text-xs font-medium"
22+
>
23+
{props.label}
24+
</label>
25+
</div>
26+
27+
<div className="grid grid-flow-row col-span-2 gap-1">
28+
<input
29+
ref={ref}
30+
className={twMerge(
31+
'bg-input px-3 py-2 text-xs text-white border border-gray-700 rounded-md',
32+
className,
33+
)}
34+
minLength={2}
35+
maxLength={128}
36+
spellCheck="false"
37+
autoCapitalize="none"
38+
{...props}
39+
/>
40+
41+
{error && <p className="text-xs text-red-500">{error}</p>}
42+
</div>
43+
</div>
44+
);
45+
});

Diff for: src/components/conferences/Agenda.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useConferencesQueryQuery } from '@/utils/__generated__/graphql';
1+
import { useFeaturedConferencesQuery } from '@/utils/__generated__/graphql';
22

33
import { Day } from './Day';
44

@@ -7,7 +7,7 @@ interface AgendaProps {
77
}
88

99
export function Agenda({ amountOfDays }: AgendaProps) {
10-
const { data, isLoading, isError } = useConferencesQueryQuery();
10+
const { data, isLoading, isError } = useFeaturedConferencesQuery();
1111

1212
if (isError) return <p>Failed to agenda</p>;
1313

Diff for: src/components/conferences/FeaturedConference.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getDatesInRange } from '@/utils/getDatesInRange';
22
import {
33
Conferences,
4-
useConferencesQueryQuery,
4+
useFeaturedConferencesQuery,
55
} from '@/utils/__generated__/graphql';
66

77
import { Loader } from '@/components/common/Loader';
@@ -24,7 +24,7 @@ export function FeaturedConferenceContainer({
2424
}
2525

2626
export function FeaturedConference() {
27-
const { data, isLoading, isError } = useConferencesQueryQuery();
27+
const { data, isLoading, isError } = useFeaturedConferencesQuery();
2828

2929
if (isError) {
3030
return (

Diff for: src/graphql/conferenceBySlug.graphql

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
query ConferenceBySlug($slug: String!) {
2+
conferences(where: { slug: { _eq: $slug } }) {
3+
id
4+
name
5+
slug
6+
location
7+
featured
8+
start_date
9+
end_date
10+
talks(order_by: { start_date: asc }) {
11+
id
12+
name
13+
start_date
14+
end_date
15+
speaker {
16+
name
17+
id
18+
social
19+
job_description
20+
avatar_url
21+
bio
22+
}
23+
}
24+
}
25+
}

Diff for: src/graphql/conferences.graphql

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
query ConferencesQuery {
2-
conferences(where: { featured: { _eq: true } }) {
1+
query Conferences($filter: String) {
2+
conferences(
3+
where: { _or: { name: { _ilike: $filter }, slug: { _ilike: $filter } } }
4+
) {
35
id
46
name
7+
slug
58
location
69
featured
710
start_date
@@ -22,3 +25,9 @@ query ConferencesQuery {
2225
}
2326
}
2427
}
28+
29+
mutation AddConference($conference: conferences_insert_input!) {
30+
insert_conferences_one(object: $conference) {
31+
id
32+
}
33+
}

Diff for: src/graphql/featuredConferences.graphql

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
query FeaturedConferences {
2+
conferences(where: { featured: { _eq: true } }) {
3+
id
4+
name
5+
slug
6+
location
7+
featured
8+
start_date
9+
end_date
10+
talks(order_by: { start_date: asc }) {
11+
id
12+
name
13+
start_date
14+
end_date
15+
speaker {
16+
name
17+
id
18+
social
19+
job_description
20+
avatar_url
21+
bio
22+
}
23+
}
24+
}
25+
}

Diff for: src/hooks/useAuthenticatedRedirect.tsx

-13
This file was deleted.

Diff for: src/layouts/AuthenticatedLayout.tsx

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useAuthenticationStatus } from '@nhost/react';
2+
import { useRouter } from 'next/router';
3+
import { useEffect } from 'react';
4+
import BaseLayout, { BaseLayoutProps } from './BaseLayout';
5+
6+
export type AuthenticatedLayoutProps = BaseLayoutProps;
7+
8+
export default function AuthenticatedLayout({
9+
children,
10+
...props
11+
}: AuthenticatedLayoutProps) {
12+
const router = useRouter();
13+
const { isLoading, isAuthenticated } = useAuthenticationStatus();
14+
15+
useEffect(() => {
16+
if (!isLoading && !isAuthenticated) {
17+
router.push('/sign-in');
18+
}
19+
}, [isLoading, isAuthenticated, router]);
20+
21+
if (isLoading || !isAuthenticated) {
22+
return <BaseLayout {...props}>Loading user...</BaseLayout>;
23+
}
24+
25+
return <BaseLayout {...props}>{children}</BaseLayout>;
26+
}

0 commit comments

Comments
 (0)