From ffc1ed9d211badb8a67d67b1f52313f3f29161a2 Mon Sep 17 00:00:00 2001 From: Adi Thakral Date: Fri, 2 Apr 2021 16:14:50 -0400 Subject: [PATCH] Hookup public page --- .gitignore | 1 + backend/main.py | 3 +- frontend/components/Editor/Link.tsx | 8 +-- frontend/components/Editor/index.tsx | 10 +++- frontend/components/Links/index.tsx | 13 ++-- frontend/components/Login/authcontext.tsx | 73 ++++++++++++++++------- frontend/components/Login/loginbox.tsx | 16 +++-- frontend/components/index.ts | 3 - frontend/next.config.js | 6 +- frontend/package.json | 4 +- frontend/pages/editor.tsx | 59 +++++++----------- frontend/pages/index.tsx | 30 +++++----- 12 files changed, 123 insertions(+), 103 deletions(-) delete mode 100644 frontend/components/index.ts diff --git a/.gitignore b/.gitignore index 0b07ba6..2a6db5c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ links.db password.txt /.vs /.vscode +/links.json diff --git a/backend/main.py b/backend/main.py index 372f23d..a967899 100644 --- a/backend/main.py +++ b/backend/main.py @@ -7,7 +7,7 @@ import os DB_PATH = os.path.join(os.path.dirname(__file__), 'links.db') app = Flask(__name__) -auth = HTTPBasicAuth() +auth = HTTPBasicAuth('CustomBasic') users = { "admin": generate_password_hash("test"), @@ -62,6 +62,7 @@ def update_links(): cur.executemany('INSERT INTO links VALUES (?,?,?,?)', links) con.commit() data = regen_JSON() + # TODO: Trigger a rebuild of the frontend outfile = open('data.json', 'w') print(data, file=outfile) outfile.close() diff --git a/frontend/components/Editor/Link.tsx b/frontend/components/Editor/Link.tsx index 0fc0ba1..6e83404 100644 --- a/frontend/components/Editor/Link.tsx +++ b/frontend/components/Editor/Link.tsx @@ -1,8 +1,8 @@ -import React, { useRef, useState } from "react"; +import React from "react"; import { Draggable } from "react-beautiful-dnd"; export type EditableLink = { - title: string; + name: string; url: string; active: boolean; clicks: number; @@ -55,8 +55,8 @@ const Link: React.FC = ({ index, link, onChange, onDelete }) => { onChange({ ...link, title: e.target.value })} + value={link.name} + onChange={(e) => onChange({ ...link, name: e.target.value })} /> >; } -const Editor: React.FC = ({ links, setLinks }) => { +const Editor: React.FC = ({ links }) => { const [formState, setFormState] = useState(links); const { displayDragDrop } = useDragDrop(); + useEffect(() => { + setFormState(links); + }, [links]); + const handleOnDragEnd = (result: DropResult) => { if (!result?.destination) return; @@ -29,7 +33,7 @@ const Editor: React.FC = ({ links, setLinks }) => { setFormState([ ...formState, { - title: "", + name: "", url: "", clicks: 0, active: true, diff --git a/frontend/components/Links/index.tsx b/frontend/components/Links/index.tsx index 67c7c4d..ce4baa1 100644 --- a/frontend/components/Links/index.tsx +++ b/frontend/components/Links/index.tsx @@ -1,15 +1,14 @@ import React from "react"; -import { useEffect } from "react"; -interface Link { - title: string; +export interface Link { + name: string; url: string; } interface LinkProps { links: Link[]; } -const Links: React.FC = ({ links }) => { +export const Links: React.FC = ({ links }) => { const postData = (url = ""): void => { fetch(url, { method: "POST", @@ -36,7 +35,7 @@ const Links: React.FC = ({ links }) => { CSC Logo

@uwcsclub

    - {links.map(({ title, url }, i) => ( + {links.map(({ name, url }, i) => (
  • - {title} + {name}
  • ))} @@ -55,5 +54,3 @@ const Links: React.FC = ({ links }) => { ); }; - -export default Links; diff --git a/frontend/components/Login/authcontext.tsx b/frontend/components/Login/authcontext.tsx index 55ce0e7..ae094d7 100644 --- a/frontend/components/Login/authcontext.tsx +++ b/frontend/components/Login/authcontext.tsx @@ -1,40 +1,71 @@ import React, { useState, useContext, createContext } from "react"; -export interface AuthContextState { - loggedIn: boolean; - loginFailed: boolean; - login: (pass?: string) => void; +interface LoggedInState { + loggedIn: true; + headers: HeadersInit; + logout(): void; } -const DEFAULT_STATE: AuthContextState = { - loggedIn: false, - loginFailed: false, - login: () => console.error("No parent AuthContext found!"), -}; +interface LoggedOutState { + loggedIn: false; + login(password: string): Promise; +} -const AuthContext: React.Context = createContext( - DEFAULT_STATE -); +export type AuthState = LoggedInState | LoggedOutState; -const password = "bubbles"; +const AuthContext = createContext({ + loggedIn: false, + login: () => { + throw new Error("No parent AuthContext found!"); + }, +} as AuthState); export const AuthProvider: React.FC = (props) => { const [loggedIn, setLoggedIn] = useState(false); - const [loginFailed, setLoginFailed] = useState(false); + const [headers, setHeaders] = useState(); + + function logout() { + setLoggedIn(false); + setHeaders(undefined); + } - function login(pass?: string): void { - if (pass === password) { + async function login(password: string): Promise { + const username = process.env.NEXT_PUBLIC_EDITOR_USERNAME; + + if (!username) { + throw new Error( + "Missing NEXT_PUBLIC_EDITOR_USERNAME environment variable" + ); + } + + const newHeaders = { + Authorization: `CustomBasic ${btoa(`${username}:${password}`)}`, + }; + + const res = await fetch("/api/editor/links", { headers: newHeaders }); + + if (res.status === 200) { setLoggedIn(true); - setLoginFailed(false); + setHeaders(newHeaders); + return true; } else { - setLoggedIn(false); - setLoginFailed(true); + logout(); + return false; } } return ( - + ); }; -export const useAuth = () => useContext(AuthContext); +export function useAuth(): AuthState { + return useContext(AuthContext); +} diff --git a/frontend/components/Login/loginbox.tsx b/frontend/components/Login/loginbox.tsx index fcaba99..8681e0c 100644 --- a/frontend/components/Login/loginbox.tsx +++ b/frontend/components/Login/loginbox.tsx @@ -4,7 +4,8 @@ import { useAuth } from "components/Login/authcontext"; const LoginBox: React.FC = () => { const [password, setPassword] = useState(""); const [focused, setFocused] = useState(false); - const { login, loginFailed } = useAuth(); + const [loginFailed, setLoginFailed] = useState(false); + const auth = useAuth(); const passwordLabelClassName = `absolute inset-y-0 left-0 px-4 font-sans text-gray-600 ${ focused || password @@ -12,10 +13,17 @@ const LoginBox: React.FC = () => { : "" } transition-transform pointer-events-none`; - function handleSubmit(e: React.SyntheticEvent): void { + async function handleSubmit(e: React.SyntheticEvent) { e.preventDefault(); - login(password); - setPassword(""); + + if (!auth.loggedIn) { + const loginSuccessful = await auth.login(password); + + if (!loginSuccessful) { + setLoginFailed(true); + setPassword(""); + } + } } return ( diff --git a/frontend/components/index.ts b/frontend/components/index.ts deleted file mode 100644 index 0914f5a..0000000 --- a/frontend/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as Links } from "./Links"; -export { default as Editor } from "./Editor"; -export { default as Preview } from "./Preview"; diff --git a/frontend/next.config.js b/frontend/next.config.js index 6f31c75..cfd6b99 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -10,11 +10,11 @@ const devConfig = { return [ { source: "/api", - destination: "http://localhost:5000/editor/links", + destination: "http://localhost:5000", }, { - source: "/api/:slug", - destination: "http://localhost:5000/:slug", + source: "/api/:path*", + destination: "http://localhost:5000/:path*", }, ]; }, diff --git a/frontend/package.json b/frontend/package.json index 8bb1070..adb188d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,12 +6,14 @@ "dev": "next dev", "build": "next build", "start": "next start", + "type-check": "tsc", "format": "prettier --write './**/*'", "format:check": "prettier --check './**/*'", "lint": "eslint \"{pages,components}/**/*.{js,ts,tsx,jsx}\" --quiet --fix", "lint:check": "eslint \"{pages,components}/**/*.{js,ts,tsx,jsx}\" --quiet", "check": "npm run format:check && npm run lint:check", - "check:fix": "npm run format && npm run lint" + "check:fix": "npm run format && npm run lint", + "clean-cache": "rm -rf ./.next" }, "dependencies": { "fast-deep-equal": "^3.1.3", diff --git a/frontend/pages/editor.tsx b/frontend/pages/editor.tsx index 9954c47..fffdb50 100644 --- a/frontend/pages/editor.tsx +++ b/frontend/pages/editor.tsx @@ -1,6 +1,5 @@ import Head from "next/head"; -import { GetStaticProps } from "next"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { AuthProvider, useAuth } from "components/Login/authcontext"; import LoginHead from "components/Login/loginhead"; import LoginBox from "components/Login/loginbox"; @@ -8,33 +7,6 @@ import Analytics from "components/Analytics"; import Editor from "components/Editor"; import { EditableLink } from "components/Editor/Link"; -export const getStaticProps: GetStaticProps = async () => { - // TODO: Fetch links here - - return { - props: { - data: [ - { - title: "dummlink1", - url: "www.helloworld.com", - clicks: 0, - active: true, - }, - { - title: "dummlink2", - url: "www.hiworld.com", - clicks: 0, - active: true, - }, - ], - }, // will be passed to the page component as props - // Next.js will attempt to re-generate the page: - // - When a request comes intype EditableLink = { - // - At most once every second - revalidate: 1, - }; -}; - const LoginScreen: React.FC = () => (
    @@ -51,16 +23,25 @@ const LoginScreen: React.FC = () => (
    ); -interface EditorPageProps { - data: any; -} +const EditorPage: React.FC = () => { + const auth = useAuth(); + const [links, setLinks] = useState([]); + + useEffect(() => { + async function fetchLinks() { + if (!auth.loggedIn) { + return; + } + + const res = await fetch("/api/editor/links", { headers: auth.headers }); + + setLinks(await res.json()); + } -const EditorPage: React.FC = ({ data }) => { - const { loggedIn } = useAuth(); - const [links, setLinks] = useState(data ?? []); + fetchLinks(); + }, [auth]); - console.log({ links }); - return loggedIn ? ( + return auth.loggedIn ? ( <> @@ -70,10 +51,10 @@ const EditorPage: React.FC = ({ data }) => { ); }; -export default function EditorPageWrapper({ data }: any): JSX.Element { +export default function EditorPageWrapper(): JSX.Element { return ( - + ); } diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 9c5e180..6ea2095 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,29 +1,27 @@ import React from "react"; import { GetStaticProps } from "next"; -import { Links } from "components"; +import { Link, Links } from "components/Links"; +import { readFileSync } from "fs"; -// TODO: change -const API = "https://api.thedogapi.com/v1/breeds?limit=10&page=0"; +export const getStaticProps: GetStaticProps = async () => { + if (!process.env.LINKS_FILE) { + throw new Error("Set the LINKS_FILE environment variable"); + } -export const getStaticProps: GetStaticProps = async () => { - // fetch data here - const data = await fetch(API).then((res) => res.json()); + const links = JSON.parse(readFileSync(process.env.LINKS_FILE).toString()); return { - props: { data }, // will be passed to the page component as props + props: { links }, revalidate: 1, }; }; -const Home: React.FC = ({ data }: any) => { - return ( - ({ - title: dog.name, - url: "https://www.google.com/", - }))} - /> - ); +interface Props { + links: Link[]; +} + +const Home: React.FC = ({ links }) => { + return ; }; export default Home;