Hookup update links endpoint

This commit is contained in:
Steven Xu 2021-04-03 21:01:00 -04:00
parent c17dff1792
commit 413a00259c
13 changed files with 217 additions and 242 deletions

1
.gitignore vendored
View File

@ -4,4 +4,3 @@ password.txt
/.vs
/.vscode
data.json
/links.json

View File

@ -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<AnalyticsProps> = ({ clicks }) => {
return (
<div className="w-full h-12 lt-lg:h-16 mr-0 px-4 lt-lg:px-6 flex flex-row border-b border-analytics-border text-sm font-karla">
<div className="w-full h-full flex-analytics flex flex-row justify-start items-center">
<span className="mr-4 font-bold">Lifetime Analytics:</span>
<div className="mr-8 flex flex-row justify-center items-center">
<div className="h-2 w-2 mr-2 rounded bg-analytics-view-icon"></div>
<div className="flex flex-col lt-lg:flex-row text-xs lt-lg:text-base font-normal">
<p className="whitespace-pre">Views: </p>
<p className="font-bold lt-lg:font-normal">{viewCount || "-"}</p>
</div>
</div>
<div className="mr-8 flex flex-row justify-center items-center">
<div className="h-2 w-2 mr-2 rounded bg-analytics-click-icon"></div>
<div className="flex flex-col lt-lg:flex-row text-xs lt-lg:text-base font-normal">
<p className="whitespace-pre">Clicks: </p>
<p className="font-bold lt-lg:font-normal">{clickCount || "-"}</p>
<p className="font-bold lt-lg:font-normal">{clicks || "-"}</p>
</div>
</div>
<div>{Question}</div>

View File

@ -64,7 +64,7 @@ const Link: React.FC<LinkProps> = ({ index, link, onChange, onDelete }) => {
onChange={(e) =>
onChange({ ...link, active: e.target.checked })
}
checked={link.active}
defaultChecked={link.active}
className="float-right"
/>
</div>

View File

@ -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<React.SetStateAction<EditableLink[]>>;
}
const Editor: React.FC<EditorProps> = ({ links }) => {
const Editor: React.FC<EditorProps> = ({ links, setLinks }) => {
const [formState, setFormState] = useState<EditableLink[]>(links);
const { displayDragDrop } = useDragDrop();
const { headers } = useAuth();
useEffect(() => {
setFormState(links);
@ -40,18 +42,27 @@ const Editor: React.FC<EditorProps> = ({ 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 (
<div className="space-y-4 bg-gray-100 w-1/2 p-2">
@ -97,6 +108,24 @@ const Editor: React.FC<EditorProps> = ({ links }) => {
</Droppable>
</DragDropContext>
)}
<div className="mb-16" />
<div className="flex">
<button
className="block flex py-2 items-center justify-center rounded-md bg-purple-600 hover:bg-purple-500 cursor-pointer text-white w-full disabled:opacity-50"
onClick={onSubmit}
disabled={equal(formState, links)}
>
Submit
</button>
<div className="mr-4" />
<button
className="block flex py-2 items-center justify-center rounded-md bg-purple-600 hover:bg-purple-500 cursor-pointer text-white w-full disabled:opacity-50"
onClick={onCancel}
disabled={equal(formState, links)}
>
Cancel
</button>
</div>
</div>
);
};

View File

@ -22,13 +22,14 @@ export const Links: React.FC<LinkProps> = ({ 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 (
<div className="text-s flex flex-col items-center w-full absolute top-6 font-karla">
@ -44,7 +45,7 @@ export const Links: React.FC<LinkProps> = ({ links }) => {
href={url}
target="_blank"
rel="noreferrer"
onClick={handleClick}
onClick={() => handleClick(name, url)}
>
{name}
</a>

View File

@ -0,0 +1,56 @@
import React, { useState, useContext, createContext } from "react";
interface AuthState {
loggedIn: boolean;
login: (password: string) => Promise<boolean>;
logout: () => void;
headers?: HeadersInit;
}
const AuthContext = createContext<AuthState>({
loggedIn: false,
login: () => Promise.resolve(false),
logout: () => console.error("No parent AuthContext found!"),
});
export const AuthProvider: React.FC = ({ children }) => {
const [headers, setHeaders] = useState<HeadersInit | undefined>();
const logout = () => setHeaders(undefined);
const login = async (password: string): Promise<boolean> => {
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 (
<AuthContext.Provider
value={{ loggedIn: !!headers, login, logout, headers }}
>
{children}
</AuthContext.Provider>
);
};
export function useAuth(): AuthState {
return useContext(AuthContext);
}

View File

@ -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<boolean>;
}
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<HeadersInit | undefined>();
function logout() {
setLoggedIn(false);
setHeaders(undefined);
}
async function login(password: string): Promise<boolean> {
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 (
<AuthContext.Provider
value={
loggedIn && headers != null
? { loggedIn, headers, logout }
: { loggedIn: false, login }
}
{...props}
/>
);
};
export function useAuth(): AuthState {
return useContext(AuthContext);
}

View File

@ -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 (
<div className="fixed h-screen w-full overflow-auto bg-gray-100">
<div className="m-auto h-full flex justify-center items-center">
<div className="container m-auto h-auto flex flex-col justify-center items-center p-10 space-y-20">
<Head>
<title>Login</title>
</Head>
<div className="flex flex-col justify-center items-center space-y-10">
<div className="flex flex-row justify-center items-center space-x-5">
<Image
src="/images/csc-logo-fb-trans.png"
height={80}
width={80}
alt="CSC Logo"
/>
<h1 className="text-4xl font-sans font-bold text-gray-900">
linklist
</h1>
</div>
<h2 className="text-xl font-sans font-semibold text-gray-900 text-center">
Log in to continue to your Linklist admin
</h2>
</div>
<div className="flex justify-center items-center px-10 py-8 bg-gray-50 border-2 border-gray-300 rounded-lg">
<div className="space-y-4">
{loginFailed ? (
<div className="text-red-600">Invalid credentials.</div>
) : null}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="password" className="relative">
<span className={passwordLabelClassName}>Password</span>
</label>
<input
name="password"
type="password"
value={password}
onFocus={() => 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"
/>
</div>
<input
type="submit"
value="Log In"
className="w-full px-4 py-2 font-sans font-semibold text-white bg-purple-700 focus:outline-none focus:ring-4 focus:ring-purple-300 rounded-lg"
/>
</form>
</div>
</div>
</div>
</div>
</div>
);
};
export default LoginBox;

View File

@ -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 (
<div className="space-y-4">
{loginFailed ? (
<div className="text-red-600">Invalid credentials.</div>
) : null}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="password" className="relative">
<span className={passwordLabelClassName}>Password</span>
</label>
<input
name="password"
type="password"
value={password}
onFocus={() => 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"
/>
</div>
<input
type="submit"
value="Log In"
className="w-full px-4 py-2 font-sans font-semibold text-white bg-purple-700 focus:outline-none focus:ring-4 focus:ring-purple-300 rounded-lg"
/>
</form>
</div>
);
};
export default LoginBox;

View File

@ -1,22 +0,0 @@
import Image from "next/image";
const LoginHead: React.FC = () => {
return (
<div className="flex flex-col justify-center items-center space-y-10">
<div className="flex flex-row justify-center items-center space-x-5">
<Image
src="/images/csc-logo-fb-trans.png"
height={80}
width={80}
alt="CSC Logo"
/>
<h1 className="text-4xl font-sans font-bold text-gray-900">linklist</h1>
</div>
<h2 className="text-xl font-sans font-semibold text-gray-900 text-center">
Log in to continue to your Linklist admin
</h2>
</div>
);
};
export default LoginHead;

View File

@ -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 = () => (
<div className="fixed h-screen w-full overflow-auto bg-gray-100">
<div className="m-auto h-full flex justify-center items-center">
<div className="container m-auto h-auto flex flex-col justify-center items-center p-10 space-y-20">
<Head>
<title>Login</title>
</Head>
<LoginHead />
<div className="flex justify-center items-center px-10 py-8 bg-gray-50 border-2 border-gray-300 rounded-lg">
<LoginBox />
</div>
</div>
</div>
</div>
);
const EditorPage: React.FC = () => {
const auth = useAuth();
const { loggedIn, headers } = useAuth();
const [links, setLinks] = useState<EditableLink[]>([]);
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 ? (
<>
<Analytics />
<Analytics clicks={links.reduce((acc, curr) => acc + curr.clicks, 0)} />
<Editor links={links} setLinks={setLinks} />
</>
) : (
<LoginScreen />
<Login />
);
};

View File

@ -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<Props> = 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,
};
};

View File

@ -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: [],
};