diff --git a/.gitignore b/.gitignore index f57bc15..358aebf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ password.txt /.vs /.vscode data.json -/links.json diff --git a/frontend/components/Analytics/index.tsx b/frontend/components/Analytics/index.tsx index 80f0e12..580f477 100644 --- a/frontend/components/Analytics/index.tsx +++ b/frontend/components/Analytics/index.tsx @@ -1,41 +1,20 @@ import React from "react"; -import { useState, useEffect } from "react"; import { Question, Chevron } from "./assets"; -const Analytics: React.FC = () => { - const [viewCount, setViewCount] = useState(0); - const [clickCount, setClickCount] = useState(0); - - useEffect(() => { - fetch("https://dog.ceo/api/breeds/list/all") // TODO: Change to '/api/editor/links' - .then((results) => results.json()) - .then((data) => { - console.log("Success:", data); - // TODO: Assign the correct values here: - // setViewCount(data.views); - // setClickCount(data.clicks); - }) - .catch((error) => { - console.error("Error:", error); - }); - }, []); +interface AnalyticsProps { + clicks: number; +} +const Analytics: React.FC = ({ clicks }) => { return (
Lifetime Analytics: -
-
-
-

Views:

-

{viewCount || "-"}

-
-

Clicks:

-

{clickCount || "-"}

+

{clicks || "-"}

{Question}
diff --git a/frontend/components/Editor/Link.tsx b/frontend/components/Editor/Link.tsx index 6e83404..536d8ed 100644 --- a/frontend/components/Editor/Link.tsx +++ b/frontend/components/Editor/Link.tsx @@ -64,7 +64,7 @@ const Link: React.FC = ({ index, link, onChange, onDelete }) => { onChange={(e) => onChange({ ...link, active: e.target.checked }) } - checked={link.active} + defaultChecked={link.active} className="float-right" />
diff --git a/frontend/components/Editor/index.tsx b/frontend/components/Editor/index.tsx index e452fe0..5c2048f 100644 --- a/frontend/components/Editor/index.tsx +++ b/frontend/components/Editor/index.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { DragDropContext, Droppable, DropResult } from "react-beautiful-dnd"; import Link, { EditableLink } from "components/Editor/Link"; +import { useAuth } from "components/Login/AuthContext"; import { useDragDrop } from "./useDragDrop"; import equal from "fast-deep-equal"; @@ -10,9 +11,10 @@ interface EditorProps { setLinks: React.Dispatch>; } -const Editor: React.FC = ({ links }) => { +const Editor: React.FC = ({ links, setLinks }) => { const [formState, setFormState] = useState(links); const { displayDragDrop } = useDragDrop(); + const { headers } = useAuth(); useEffect(() => { setFormState(links); @@ -40,18 +42,27 @@ const Editor: React.FC = ({ links }) => { }, ]); - // useEffect(() => { - // setFormState(links); - // }, [links]); + useEffect(() => { + setFormState(links); + }, [links]); - const onSubmit = () => { + const onSubmit = async () => { // const res = await updateLinks(formState); - // setLinks(res.data); + const res = await fetch("/api/editor/links", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...headers, + }, + body: JSON.stringify({ links: formState }), + }); + const updatedLinks = await res.json(); + setLinks(updatedLinks); }; - const didEdit = !equal(formState, links); - - console.log({ formState, didEdit }); + const onCancel = () => { + setFormState(links); + }; return (
@@ -97,6 +108,24 @@ const Editor: React.FC = ({ links }) => { )} +
+
+ +
+ +
); }; diff --git a/frontend/components/Links/index.tsx b/frontend/components/Links/index.tsx index ce4baa1..9cc369a 100644 --- a/frontend/components/Links/index.tsx +++ b/frontend/components/Links/index.tsx @@ -22,13 +22,14 @@ export const Links: React.FC = ({ links }) => { }); }; - // useEffect((): void => { - // postData("https://dog.ceo/api/breeds/list/all"); // TODO: Change to '/api/view' - // }, []); - - const handleClick = (): void => { - postData("https://dog.ceo/api/breeds/list/all"); // TODO: Change to '/api/click' - }; + const handleClick = (name: string, url: string) => + fetch("/api/clicks", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name, url }), + }); return (
@@ -44,7 +45,7 @@ export const Links: React.FC = ({ links }) => { href={url} target="_blank" rel="noreferrer" - onClick={handleClick} + onClick={() => handleClick(name, url)} > {name} diff --git a/frontend/components/Login/AuthContext.tsx b/frontend/components/Login/AuthContext.tsx new file mode 100644 index 0000000..d85541b --- /dev/null +++ b/frontend/components/Login/AuthContext.tsx @@ -0,0 +1,56 @@ +import React, { useState, useContext, createContext } from "react"; + +interface AuthState { + loggedIn: boolean; + login: (password: string) => Promise; + logout: () => void; + headers?: HeadersInit; +} + +const AuthContext = createContext({ + loggedIn: false, + login: () => Promise.resolve(false), + logout: () => console.error("No parent AuthContext found!"), +}); + +export const AuthProvider: React.FC = ({ children }) => { + const [headers, setHeaders] = useState(); + + const logout = () => setHeaders(undefined); + + const login = async (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) { + setHeaders(newHeaders); + return true; + } else { + logout(); + return false; + } + }; + + return ( + + {children} + + ); +}; + +export function useAuth(): AuthState { + return useContext(AuthContext); +} diff --git a/frontend/components/Login/authcontext.tsx b/frontend/components/Login/authcontext.tsx deleted file mode 100644 index ae094d7..0000000 --- a/frontend/components/Login/authcontext.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useState, useContext, createContext } from "react"; - -interface LoggedInState { - loggedIn: true; - headers: HeadersInit; - logout(): void; -} - -interface LoggedOutState { - loggedIn: false; - login(password: string): Promise; -} - -export type AuthState = LoggedInState | LoggedOutState; - -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 [headers, setHeaders] = useState(); - - function logout() { - setLoggedIn(false); - setHeaders(undefined); - } - - 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); - setHeaders(newHeaders); - return true; - } else { - logout(); - return false; - } - } - - return ( - - ); -}; - -export function useAuth(): AuthState { - return useContext(AuthContext); -} diff --git a/frontend/components/Login/index.tsx b/frontend/components/Login/index.tsx new file mode 100644 index 0000000..53137fa --- /dev/null +++ b/frontend/components/Login/index.tsx @@ -0,0 +1,91 @@ +import React, { useState } from "react"; +import { useAuth } from "components/Login/AuthContext"; +import Image from "next/image"; +import Head from "next/head"; + +const LoginBox: React.FC = () => { + const [password, setPassword] = useState(""); + const [focused, setFocused] = useState(false); + const [loginFailed, setLoginFailed] = useState(false); + const { loggedIn, login } = useAuth(); + + const passwordLabelClassName = `absolute inset-y-0 left-0 px-4 font-sans text-gray-600 ${ + focused || password + ? "transform scale-75 -translate-y-5 -translate-x-2" + : "" + } transition-transform pointer-events-none`; + + const handleSubmit = async (e: React.SyntheticEvent) => { + e.preventDefault(); + + if (!loggedIn) { + const loginSuccessful = await login(password); + + if (!loginSuccessful) { + setLoginFailed(true); + setPassword(""); + } + } + }; + + return ( +
+
+
+ + Login + +
+
+ CSC Logo +

+ linklist +

+
+

+ Log in to continue to your Linklist admin +

+
+
+
+ {loginFailed ? ( +
Invalid credentials.
+ ) : null} +
+
+ + setFocused(true)} + onBlur={() => { + setFocused(false); + setLoginFailed(false); + }} + onChange={(event) => setPassword(event.target.value)} + className="bg-transparent p-4 border border-gray-300 leading-snug focus:outline-none focus:border-gray-500 rounded" + /> +
+ +
+
+
+
+
+
+ ); +}; + +export default LoginBox; diff --git a/frontend/components/Login/loginbox.tsx b/frontend/components/Login/loginbox.tsx deleted file mode 100644 index 8681e0c..0000000 --- a/frontend/components/Login/loginbox.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useState } from "react"; -import { useAuth } from "components/Login/authcontext"; - -const LoginBox: React.FC = () => { - const [password, setPassword] = useState(""); - const [focused, setFocused] = useState(false); - 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 - ? "transform scale-75 -translate-y-5 -translate-x-2" - : "" - } transition-transform pointer-events-none`; - - async function handleSubmit(e: React.SyntheticEvent) { - e.preventDefault(); - - if (!auth.loggedIn) { - const loginSuccessful = await auth.login(password); - - if (!loginSuccessful) { - setLoginFailed(true); - setPassword(""); - } - } - } - - return ( -
- {loginFailed ? ( -
Invalid credentials.
- ) : null} -
-
- - setFocused(true)} - onBlur={() => setFocused(false)} - onChange={(event) => setPassword(event.target.value)} - className="bg-transparent p-4 border border-gray-300 leading-snug focus:outline-none focus:border-gray-500 rounded" - /> -
- -
-
- ); -}; - -export default LoginBox; diff --git a/frontend/components/Login/loginhead.tsx b/frontend/components/Login/loginhead.tsx deleted file mode 100644 index 950e223..0000000 --- a/frontend/components/Login/loginhead.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Image from "next/image"; - -const LoginHead: React.FC = () => { - return ( -
-
- CSC Logo -

linklist

-
-

- Log in to continue to your Linklist admin -

-
- ); -}; - -export default LoginHead; diff --git a/frontend/pages/editor.tsx b/frontend/pages/editor.tsx index fffdb50..478106a 100644 --- a/frontend/pages/editor.tsx +++ b/frontend/pages/editor.tsx @@ -1,53 +1,33 @@ -import Head from "next/head"; 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"; +import { AuthProvider, useAuth } from "components/Login/AuthContext"; +import Login from "components/Login"; import Analytics from "components/Analytics"; import Editor from "components/Editor"; import { EditableLink } from "components/Editor/Link"; -const LoginScreen: React.FC = () => ( -
-
-
- - Login - - -
- -
-
-
-
-); - const EditorPage: React.FC = () => { - const auth = useAuth(); + const { loggedIn, headers } = useAuth(); const [links, setLinks] = useState([]); useEffect(() => { async function fetchLinks() { - if (!auth.loggedIn) { - return; - } + if (!loggedIn) return; - const res = await fetch("/api/editor/links", { headers: auth.headers }); - - setLinks(await res.json()); + const res = await fetch("/api/editor/links", { headers }); + const links = await res.json(); + setLinks(links); } fetchLinks(); - }, [auth]); + }, [loggedIn, headers]); - return auth.loggedIn ? ( + return loggedIn ? ( <> - + acc + curr.clicks, 0)} /> ) : ( - + ); }; diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 6ea2095..a88b6d8 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,18 +1,14 @@ import React from "react"; import { GetStaticProps } from "next"; import { Link, Links } from "components/Links"; -import { readFileSync } from "fs"; export const getStaticProps: GetStaticProps = async () => { - if (!process.env.LINKS_FILE) { - throw new Error("Set the LINKS_FILE environment variable"); - } - - const links = JSON.parse(readFileSync(process.env.LINKS_FILE).toString()); + const res = await fetch(`${process.env.DEV_URL}/links`); + const links = await res.json(); return { props: { links }, - revalidate: 1, + revalidate: 60, }; }; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 0f2ab6e..a3988d2 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -39,18 +39,14 @@ module.exports = { chevron: "0 0 16px", }, }, - minWidth: { - "9/10": "90%", - }, - maxWidth: { - "6/10": "60%", - }, container: { center: true, }, }, variants: { - extend: {}, + extend: { + opacity: ["disabled"], + }, }, plugins: [], };