Compare commits
11 Commits
hook-up-ba
...
main
Author | SHA1 | Date |
---|---|---|
Aditya Thakral | 0548beb3bb | |
Neil Parikh | f16be2231f | |
Dora Su | 8033863a33 | |
Neil Parikh | 82a726ec60 | |
Steven Xu | 2fabf06e94 | |
Adi Thakral | 7b07f8a9dc | |
Steven Xu | ba210c7c4e | |
Steven Xu | 413a00259c | |
Yueran Zhang | c17dff1792 | |
Neil Parikh | 66dda06a1d | |
Adi Thakral | ffc1ed9d21 |
|
@ -3,3 +3,8 @@ links.db
|
|||
password.txt
|
||||
/.vs
|
||||
/.vscode
|
||||
data.json
|
||||
.env
|
||||
build/
|
||||
index.out.css
|
||||
frontend/public/index.html
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
### Steps to Deploy
|
||||
- move contents of frontend/ to /srv/www-csc-links/
|
||||
- create a `.env` file in backend/ with following contents:
|
||||
```
|
||||
PASSWORD=...
|
||||
PORT=...
|
||||
```
|
||||
- run the following in backend/:
|
||||
```
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
- run python app in backend/ (with the virtual env activated)
|
||||
- edit the `.htaccess` file in /srv/www-csc-links/ to point to the running python application
|
100
backend/main.py
100
backend/main.py
|
@ -1,33 +1,51 @@
|
|||
from flask import Flask, request, jsonify
|
||||
from flask import Flask, request, jsonify, render_template
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import json
|
||||
import sqlite3
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(__file__), 'links.db')
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
auth = HTTPBasicAuth()
|
||||
auth = HTTPBasicAuth('CustomBasic')
|
||||
|
||||
password = os.environ.get("PASSWORD")
|
||||
if not password:
|
||||
raise Exception("PASSWORD must be set")
|
||||
|
||||
port = int(os.environ.get("PORT") or 3000)
|
||||
|
||||
out_path = os.environ.get("OUT_PATH") or "/srv/www-csc-links/index.html"
|
||||
|
||||
users = {
|
||||
"admin": generate_password_hash("test"),
|
||||
"admin": generate_password_hash(password),
|
||||
}
|
||||
|
||||
def regen_JSON():
|
||||
"""Gets links from DB and outputs them in JSON"""
|
||||
def get_data_from_query(query):
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
cur.execute('SELECT url, name FROM links ORDER BY position')
|
||||
cur.execute(query)
|
||||
|
||||
links_list = []
|
||||
for row in cur.fetchall():
|
||||
d = dict(zip(row.keys(), row))
|
||||
links_list.append(d)
|
||||
links_json = json.dumps(links_list, indent=4)
|
||||
d = dict(zip(row.keys(), row))
|
||||
links_list.append(d)
|
||||
|
||||
con.close()
|
||||
return links_json
|
||||
return links_list
|
||||
|
||||
def regen_html(path):
|
||||
"""Gets links from DB and outputs them in HTML"""
|
||||
outfile = open(path, 'w')
|
||||
links_list = get_data_from_query('SELECT url, name FROM links WHERE active=1 ORDER BY position')
|
||||
html = render_template('template.html', links_list=links_list)
|
||||
print(html, file=outfile)
|
||||
outfile.close()
|
||||
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
|
@ -35,55 +53,55 @@ def verify_password(username, password):
|
|||
check_password_hash(users.get(username), password):
|
||||
return username
|
||||
|
||||
@app.route('/editor/links', methods = ['POST'])
|
||||
@app.route('/links', methods = ['GET'])
|
||||
def get_links():
|
||||
links_list = get_data_from_query('SELECT url, name FROM links WHERE active=1 ORDER BY position')
|
||||
return jsonify(links_list)
|
||||
|
||||
@app.route('/editor/links', methods = ['POST'])
|
||||
@auth.login_required
|
||||
def update_links():
|
||||
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
cur = con.cursor()
|
||||
try:
|
||||
cur.execute("begin")
|
||||
cur.execute('DELETE FROM links')
|
||||
|
||||
links = []
|
||||
data = request.json['links']
|
||||
items = 'url', 'name', 'clicks', 'position'
|
||||
|
||||
items = 'url', 'name', 'clicks', 'active'
|
||||
for i in range(len(data)):
|
||||
if not(all(e in data[i] for e in items)):
|
||||
if not(all(e in data[i] for e in items)):
|
||||
return "Bad request, some items missing from link object", 400
|
||||
|
||||
links = []
|
||||
cur.execute("begin")
|
||||
cur.execute('DELETE FROM links')
|
||||
for i in range(len(data)):
|
||||
url = data[i]['url']
|
||||
name = data[i]['name']
|
||||
clicks = data[i]['clicks']
|
||||
position = data[i]['position'] # TODO
|
||||
active = data[i]['active']
|
||||
position = i
|
||||
|
||||
newlink = (url, name, clicks, position)
|
||||
newlink = (url, name, clicks, position, active)
|
||||
links.append(newlink)
|
||||
|
||||
cur.executemany('INSERT INTO links VALUES (?,?,?,?)', links)
|
||||
cur.executemany('INSERT INTO links VALUES (?,?,?,?,?)', links)
|
||||
con.commit()
|
||||
data = regen_JSON()
|
||||
outfile = open('data.json', 'w')
|
||||
print(data, file=outfile)
|
||||
outfile.close()
|
||||
|
||||
except:
|
||||
con.close()
|
||||
regen_html(out_path)
|
||||
except Exception as e:
|
||||
cur.execute("rollback")
|
||||
return "done updating links"
|
||||
con.close()
|
||||
raise e
|
||||
|
||||
@app.route('/editor/links', methods = ['GET'])
|
||||
links_list = get_data_from_query('SELECT name, url, clicks, active FROM links ORDER BY position')
|
||||
return jsonify(links_list)
|
||||
|
||||
@app.route('/editor/links', methods = ['GET'])
|
||||
@auth.login_required
|
||||
def get_links():
|
||||
# endpoint lists all URLs and clicks, returns json object for editor.
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
cur = con.cursor()
|
||||
cur.execute('SELECT position, name, url, clicks FROM links ORDER BY position')
|
||||
|
||||
links_list = []
|
||||
for row in cur.fetchall():
|
||||
d = dict(zip(["position", "name", "url", "clicks"], [row[0], row[1], row[2], row[3]]))
|
||||
links_list.append(d)
|
||||
|
||||
con.close()
|
||||
def get_editor_links():
|
||||
"""endpoint lists all URLs and clicks, returns json object for editor."""
|
||||
links_list = get_data_from_query('SELECT name, url, clicks, active FROM links ORDER BY position')
|
||||
return jsonify(links_list)
|
||||
|
||||
@app.route('/clicks', methods=['POST'])
|
||||
|
@ -101,4 +119,4 @@ def update_clicks():
|
|||
return 'ok'
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
||||
app.run(port=port, host="0.0.0.0")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
astroid==2.5.1
|
||||
click==7.1.2
|
||||
Flask==1.1.2
|
||||
Flask-HTTPAuth==4.2.0
|
||||
isort==5.7.0
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.11.3
|
||||
|
@ -8,7 +9,9 @@ lazy-object-proxy==1.5.2
|
|||
MarkupSafe==1.1.1
|
||||
mccabe==0.6.1
|
||||
pylint==2.7.2
|
||||
python-dotenv==0.17.1
|
||||
toml==0.10.2
|
||||
typed-ast==1.4.3
|
||||
types-click==0.1.4
|
||||
types-Flask==0.1.1
|
||||
types-Jinja2==0.1.0
|
||||
|
@ -17,4 +20,3 @@ types-typing-extensions==3.7.2
|
|||
types-Werkzeug==0.1.1
|
||||
Werkzeug==1.0.1
|
||||
wrapt==1.12.1
|
||||
Flask-HTTPAuth==4.2.0
|
||||
|
|
|
@ -7,11 +7,11 @@ con = sqlite3.connect(DB_PATH)
|
|||
|
||||
# array of links to store
|
||||
links = [
|
||||
('http://csclub.uwaterloo.ca/','CS Club Website',3,0),
|
||||
('https://www.instagram.com/uwcsclub/','Instagram',4,1),
|
||||
('https://www.facebook.com/uw.computerscienceclub','Facebook',5,2),
|
||||
('http://twitch.tv/uwcsclub','Twitch',6,3),
|
||||
('http://bit.ly/uwcsclub-yt','YouTube',7,4),
|
||||
('http://csclub.uwaterloo.ca/','CS Club Website',0,0,1),
|
||||
('https://www.instagram.com/uwcsclub/','Instagram',0,1,1),
|
||||
('https://www.facebook.com/uw.computerscienceclub','Facebook',0,2,1),
|
||||
('http://twitch.tv/uwcsclub','Twitch',0,3,1),
|
||||
('http://bit.ly/uwcsclub-yt','YouTube',0,4,1),
|
||||
]
|
||||
|
||||
# SQLite setup
|
||||
|
@ -23,13 +23,14 @@ if cur.fetchone():
|
|||
raise Exception('Links table already exists.')
|
||||
else:
|
||||
cur.execute('''CREATE TABLE links (
|
||||
url text NOT NULL,
|
||||
name text NOT NULL,
|
||||
clicks int NOT NULL,
|
||||
url text NOT NULL,
|
||||
name text NOT NULL,
|
||||
clicks int NOT NULL,
|
||||
position int NOT NULL UNIQUE,
|
||||
active int NOT NULL,
|
||||
UNIQUE(url, name)
|
||||
)''')
|
||||
cur.executemany('INSERT INTO links VALUES (?,?,?,?)', links)
|
||||
cur.executemany('INSERT INTO links VALUES (?,?,?,?,?)', links)
|
||||
con.commit()
|
||||
|
||||
con.close()
|
||||
con.close()
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<meta charSet="utf-8"/>
|
||||
<title>@uwcsclub | LinkList</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Karla:wght@700&display=swap" rel="stylesheet">
|
||||
<link href="index.out.css" rel="stylesheet">
|
||||
<script>
|
||||
const handleClick = function(name, url) {
|
||||
fetch("api/clicks", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ name, url }),
|
||||
})
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="__next">
|
||||
<div class="text-s flex flex-col items-center w-full top-6 font-karla">
|
||||
<img class="mb-3" src="images/csc-logo.png" alt="CSC Logo" width="100px" />
|
||||
<h1 class="font-bold">@uwcsclub</h1>
|
||||
<ul class="flex flex-col my-6 w-full">
|
||||
{% for link in links_list %}
|
||||
<li class="w-full flex justify-center">
|
||||
<a
|
||||
class="btn bg-gray-450 p-3 text-white font-karla font-bold text-center self-center my-1.5
|
||||
hover:bg-white hover:text-black border-2 border-gray-800 transition duration-300 ease-in-out
|
||||
w-11/12 sm:w-4/12 min-h-link"
|
||||
href="{{ link['url'] }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onclick="return handleClick("{{ link['name'] }}", "{{ link['url'] }}")"
|
||||
>
|
||||
{{ link["name"] }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
rm -rf build
|
||||
mkdir build
|
||||
|
||||
# Build Frontend
|
||||
cd frontend/public
|
||||
NODE_ENV=production npx tailwindcss-cli@latest build index.in.css -o index.out.css
|
||||
cd ..
|
||||
npm run build && npm run export
|
||||
cd ..
|
||||
mv frontend/out build/frontend
|
||||
|
||||
# Backend
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
if [ ! -f links.db ]; then
|
||||
python3 setup_db.py
|
||||
fi
|
||||
cd ..
|
||||
rsync -ax --exclude venv backend/ build/backend
|
||||
|
||||
cp README-deploy.md build
|
|
@ -1,43 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
const Question = (
|
||||
<svg
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
color="#000"
|
||||
viewBox="0 0 16 16"
|
||||
className="sc-dkIXFM jmmNux"
|
||||
width="14"
|
||||
height="14"
|
||||
>
|
||||
<path d="M8 0a8 8 0 108 8 8 8 0 00-8-8zm0 15a7 7 0 117-7 7 7 0 01-7 7zm-.75-3.49h1.53V13H7.25zm3.54-7A3.05 3.05 0 0111 5.72a2.54 2.54 0 01-.1.76 2.9 2.9 0 01-.27.62 3.75 3.75 0 01-.38.51l-.44.45-.43.42a5.58 5.58 0 00-.38.46 2.33 2.33 0 00-.27.52 1.83 1.83 0 00-.11.64v.54H7.44V10a2.57 2.57 0 01.22-1 3 3 0 01.45-.73 6.25 6.25 0 01.57-.58 7.4 7.4 0 00.55-.57 2.42 2.42 0 00.4-.65 1.82 1.82 0 00.13-.87 1.81 1.81 0 00-.49-1.19A1.63 1.63 0 008.09 4a1.91 1.91 0 00-.86.18 1.74 1.74 0 00-.59.49 1.91 1.91 0 00-.36.71 3.7 3.7 0 00-.11.9H5A3.71 3.71 0 015.2 5a3.17 3.17 0 01.61-1.1 2.85 2.85 0 011-.69A3.21 3.21 0 018.09 3a3.4 3.4 0 011.18.19 2.71 2.71 0 01.92.53 2.6 2.6 0 01.6.83z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Chevron = (
|
||||
<svg
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 12"
|
||||
className="sc-hHftDr bphBpT"
|
||||
rotate="0"
|
||||
data-test="IconComponent"
|
||||
width="16"
|
||||
height="20"
|
||||
>
|
||||
<rect
|
||||
rotate="0"
|
||||
data-test="IconComponent"
|
||||
className="sc-fKFyDc gYoavR"
|
||||
fillOpacity="0"
|
||||
fill="#000000"
|
||||
></rect>
|
||||
<polygon
|
||||
transform="translate(5.949747, 4.949747) rotate(-45.000000) translate(-5.949747, -4.949747) "
|
||||
points="4.44974747 6.44974747 4.44974747 1.44974747 2.44974747 1.44974747 2.44974747 8.44974747 9.44974747 8.44974747 9.44974747 6.44974747"
|
||||
fill="#0A0B0D"
|
||||
></polygon>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export { Question, Chevron };
|
|
@ -1,47 +1,21 @@
|
|||
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">
|
||||
<div className="w-full h-12 lt-lg:h-16 mr-0 px-4 lt-lg:px-6 flex border-b border-gray-300 text-sm font-karla">
|
||||
<div className="w-full h-full flex-analytics flex 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="mr-8 flex 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">
|
||||
<div className="flex 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>
|
||||
</div>
|
||||
<div className="w-full h-full flex-chevron flex flex-row justify-center items-center">
|
||||
{Chevron}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.input {
|
||||
width: 100%;
|
||||
padding-right: 16px;
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
import React from "react";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
import styles from "./Link.module.css";
|
||||
|
||||
export type EditableLink = {
|
||||
title: string;
|
||||
name: string;
|
||||
url: string;
|
||||
active: boolean;
|
||||
clicks: number;
|
||||
|
@ -43,7 +44,7 @@ const Link: React.FC<LinkProps> = ({ index, link, onChange, onDelete }) => {
|
|||
<Draggable key={index} draggableId={index.toString()} index={index}>
|
||||
{(provided) => (
|
||||
<div {...provided.draggableProps} ref={provided.innerRef}>
|
||||
<div className="p-2 container flex">
|
||||
<div className="py-2 flex rounded-md md:container">
|
||||
<div
|
||||
className="flex justify-center w-1/12 flex items-center border-r bg-white rounded-sm shadow-sm"
|
||||
{...provided.dragHandleProps}
|
||||
|
@ -55,8 +56,9 @@ const Link: React.FC<LinkProps> = ({ index, link, onChange, onDelete }) => {
|
|||
<input
|
||||
type="text"
|
||||
placeholder="Edit Title"
|
||||
value={link.title}
|
||||
onChange={(e) => onChange({ ...link, title: e.target.value })}
|
||||
value={link.name}
|
||||
className={styles.input}
|
||||
onChange={(e) => onChange({ ...link, name: e.target.value })}
|
||||
/>
|
||||
|
||||
<input
|
||||
|
@ -64,7 +66,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>
|
||||
|
@ -74,6 +76,7 @@ const Link: React.FC<LinkProps> = ({ index, link, onChange, onDelete }) => {
|
|||
type="url"
|
||||
placeholder="https://url"
|
||||
value={link.url}
|
||||
className={styles.input}
|
||||
onChange={(e) => onChange({ ...link, url: e.target.value })}
|
||||
/>
|
||||
|
||||
|
@ -81,6 +84,9 @@ const Link: React.FC<LinkProps> = ({ index, link, onChange, onDelete }) => {
|
|||
<DeleteIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-between items-center px-2 pb-2">
|
||||
{`Clicks: ${link.clicks}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import React, { useState } from "react";
|
||||
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 Preview from "components/Preview";
|
||||
|
||||
import { useDragDrop } from "./useDragDrop";
|
||||
import equal from "fast-deep-equal";
|
||||
|
@ -11,88 +13,129 @@ interface EditorProps {
|
|||
}
|
||||
|
||||
const Editor: React.FC<EditorProps> = ({ links, setLinks }) => {
|
||||
const [formState, setFormState] = useState<EditableLink[]>(links);
|
||||
const { displayDragDrop } = useDragDrop();
|
||||
const { headers } = useAuth();
|
||||
const [editableLinks, setEditableLinks] = useState<EditableLink[]>(links);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setEditableLinks(links);
|
||||
}, [links]);
|
||||
|
||||
const handleOnDragEnd = (result: DropResult) => {
|
||||
if (!result?.destination) return;
|
||||
|
||||
const items = Array.from(formState);
|
||||
const items = Array.from(editableLinks);
|
||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||
items.splice(result.destination.index, 0, reorderedItem);
|
||||
|
||||
setFormState(items);
|
||||
setEditableLinks(items);
|
||||
};
|
||||
|
||||
/*note that we need to make the new links name render with nothing*/
|
||||
const handleOnClickAdd = () =>
|
||||
setFormState([
|
||||
...formState,
|
||||
setEditableLinks([
|
||||
...editableLinks,
|
||||
{
|
||||
title: "",
|
||||
name: "",
|
||||
url: "",
|
||||
clicks: 0,
|
||||
active: true,
|
||||
},
|
||||
]);
|
||||
|
||||
// useEffect(() => {
|
||||
// setFormState(links);
|
||||
// }, [links]);
|
||||
const onSubmit = async () => {
|
||||
setIsSaving(true);
|
||||
|
||||
const onSubmit = () => {
|
||||
// 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: editableLinks }),
|
||||
});
|
||||
|
||||
const updatedLinks = await res.json();
|
||||
setLinks(updatedLinks);
|
||||
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const didEdit = !equal(formState, links);
|
||||
|
||||
console.log({ formState, didEdit });
|
||||
const onCancel = () => {
|
||||
setEditableLinks(links);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 bg-gray-100 w-1/2 p-2">
|
||||
{/* add link button */}
|
||||
<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"
|
||||
onClick={handleOnClickAdd}
|
||||
>
|
||||
Add New Link
|
||||
</button>
|
||||
{displayDragDrop && (
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<Droppable droppableId="links">
|
||||
{(provided) => (
|
||||
<div
|
||||
className="links"
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{formState.map((link, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
index={index}
|
||||
link={link}
|
||||
onChange={(newLink: EditableLink) =>
|
||||
setFormState([
|
||||
...formState.slice(0, index),
|
||||
newLink,
|
||||
...formState.slice(index + 1),
|
||||
])
|
||||
}
|
||||
onDelete={() =>
|
||||
setFormState([
|
||||
...formState.slice(0, index),
|
||||
...formState.slice(index + 1),
|
||||
])
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)}
|
||||
<div className="flex flex-col bg-gray-100 md:flex-row">
|
||||
<div className="space-y-4 md:w-3/5 md:border-r md:border-gray-300">
|
||||
<div className="m-8">
|
||||
<button
|
||||
className="block flex py-2 items-center justify-center rounded-lg bg-purple-600 hover:bg-purple-500 cursor-pointer text-white w-full"
|
||||
onClick={handleOnClickAdd}
|
||||
>
|
||||
Add New Link
|
||||
</button>
|
||||
<div className="mb-8" />
|
||||
{displayDragDrop && (
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<Droppable droppableId="links">
|
||||
{(provided) => (
|
||||
<div
|
||||
className="links"
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{editableLinks.map((link, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
index={index}
|
||||
link={link}
|
||||
onChange={(newLink: EditableLink) =>
|
||||
setEditableLinks([
|
||||
...editableLinks.slice(0, index),
|
||||
newLink,
|
||||
...editableLinks.slice(index + 1),
|
||||
])
|
||||
}
|
||||
onDelete={() =>
|
||||
setEditableLinks([
|
||||
...editableLinks.slice(0, index),
|
||||
...editableLinks.slice(index + 1),
|
||||
])
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)}
|
||||
<div className="mb-8" />
|
||||
<div className="flex">
|
||||
<button
|
||||
className="block flex py-2 items-center justify-center rounded-lg bg-purple-600 hover:bg-purple-500 cursor-pointer text-white w-full disabled:opacity-50"
|
||||
onClick={onSubmit}
|
||||
disabled={isSaving || equal(editableLinks, links)}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<div className="mr-4" />
|
||||
<button
|
||||
className="block flex py-2 items-center justify-center rounded-lg bg-purple-600 hover:bg-purple-500 cursor-pointer text-white w-full disabled:opacity-50"
|
||||
onClick={onCancel}
|
||||
disabled={isSaving || equal(editableLinks, links)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-8 md:none" />
|
||||
<div className="flex m-14 justify-center md:w-2/5">
|
||||
<Preview links={editableLinks.filter((link) => link.active)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,53 +1,30 @@
|
|||
import React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface Link {
|
||||
title: string;
|
||||
export interface Link {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
interface LinkProps {
|
||||
interface Props {
|
||||
links: Link[];
|
||||
}
|
||||
|
||||
const Links: React.FC<LinkProps> = ({ links }) => {
|
||||
const postData = (url = ""): void => {
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log("Success:", data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
};
|
||||
|
||||
// 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'
|
||||
};
|
||||
|
||||
export const Links: React.FC<Props> = ({ links }) => {
|
||||
return (
|
||||
<div className="text-s flex flex-col items-center w-full absolute top-6 font-karla">
|
||||
<img className="mb-3" src="csc_logo.png" alt="CSC Logo" width="100px" />
|
||||
<div className="text-s flex flex-col items-center w-full top-6 font-karla">
|
||||
<img className="mb-3" src="images/csc-logo.png" alt="CSC Logo" width="100px" />
|
||||
<h1 className="font-bold">@uwcsclub</h1>
|
||||
<ul className="flex flex-col my-6 w-full">
|
||||
{links.map(({ title, url }, i) => (
|
||||
<li key={i} className="w-full contents">
|
||||
{links.map(({ name, url }, i) => (
|
||||
<li key={i} className="w-full flex justify-center">
|
||||
<a
|
||||
className="btn bg-gray-450 p-3 text-white font-bold text-center self-center my-1.5
|
||||
hover:bg-white hover:text-black border-2 border-gray-800 transition duration-200 ease-in-out
|
||||
w-11/12 sm:w-4/12"
|
||||
className="btn bg-gray-450 p-3 text-white font-karla font-bold text-center self-center my-1.5
|
||||
hover:bg-white hover:text-black border-2 border-gray-800 transition duration-300 ease-in-out
|
||||
w-11/12 sm:w-4/12 min-h-link"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{title}
|
||||
{name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
|
@ -55,5 +32,3 @@ const Links: React.FC<LinkProps> = ({ links }) => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Links;
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
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 = "admin";
|
||||
|
||||
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);
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import React, { useState, useContext, createContext } from "react";
|
||||
|
||||
export interface AuthContextState {
|
||||
loggedIn: boolean;
|
||||
loginFailed: boolean;
|
||||
login: (pass?: string) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: AuthContextState = {
|
||||
loggedIn: false,
|
||||
loginFailed: false,
|
||||
login: () => console.error("No parent AuthContext found!"),
|
||||
};
|
||||
|
||||
const AuthContext: React.Context<AuthContextState> = createContext(
|
||||
DEFAULT_STATE
|
||||
);
|
||||
|
||||
const password = "bubbles";
|
||||
|
||||
export const AuthProvider: React.FC = (props) => {
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
const [loginFailed, setLoginFailed] = useState(false);
|
||||
|
||||
function login(pass?: string): void {
|
||||
if (pass === password) {
|
||||
setLoggedIn(true);
|
||||
setLoginFailed(false);
|
||||
} else {
|
||||
setLoggedIn(false);
|
||||
setLoginFailed(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ loggedIn, login, loginFailed }} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
|
@ -0,0 +1,90 @@
|
|||
import React, { useState } from "react";
|
||||
import { useAuth } from "components/Login/AuthContext";
|
||||
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">
|
||||
<img
|
||||
src="images/csc-logo-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;
|
|
@ -1,51 +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 { login, loginFailed } = 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`;
|
||||
|
||||
function handleSubmit(e: React.SyntheticEvent): void {
|
||||
e.preventDefault();
|
||||
login(password);
|
||||
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;
|
|
@ -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;
|
|
@ -1,7 +1,23 @@
|
|||
import { Link, Links } from "components/Links";
|
||||
import React from "react";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const Preview: React.FC = () => {
|
||||
return <div />;
|
||||
interface PreviewProps {
|
||||
links: Link[];
|
||||
}
|
||||
|
||||
const Preview: React.FC<PreviewProps> = ({ links }) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div
|
||||
className={`box-border bg-white rounded-preview border-preview border-black ${styles.parent}`}
|
||||
>
|
||||
<div className={styles.child}>
|
||||
<Links links={links} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Preview;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
.parent {
|
||||
width: 300px;
|
||||
height: 600px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.child {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
padding-right: 17px; /* Increase/decrease this value for cross-browser compatibility */
|
||||
padding-left: 7px;
|
||||
box-sizing: content-box; /* So the width will be 100% + 17px */
|
||||
}
|
||||
.child a {
|
||||
width: 90%;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export { default as Links } from "./Links";
|
||||
export { default as Editor } from "./Editor";
|
||||
export { default as Preview } from "./Preview";
|
|
@ -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*",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,13 +5,15 @@
|
|||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"export": "next export",
|
||||
"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",
|
||||
|
|
|
@ -1,79 +1,40 @@
|
|||
import Head from "next/head";
|
||||
import { GetStaticProps } from "next";
|
||||
import React, { useState } from "react";
|
||||
import { AuthProvider, useAuth } from "components/Login/authcontext";
|
||||
import LoginHead from "components/Login/loginhead";
|
||||
import LoginBox from "components/Login/loginbox";
|
||||
import React, { useEffect, useState } from "react";
|
||||
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";
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
// TODO: Fetch links here
|
||||
const EditorPage: React.FC = () => {
|
||||
const { loggedIn, headers } = useAuth();
|
||||
const [links, setLinks] = useState<EditableLink[]>([]);
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
useEffect(() => {
|
||||
async function fetchLinks() {
|
||||
if (!loggedIn) return;
|
||||
|
||||
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 res = await fetch("api/editor/links", { headers });
|
||||
const links = await res.json();
|
||||
setLinks(links);
|
||||
}
|
||||
|
||||
interface EditorPageProps {
|
||||
data: any;
|
||||
}
|
||||
fetchLinks();
|
||||
}, [loggedIn, headers]);
|
||||
|
||||
const EditorPage: React.FC<EditorPageProps> = ({ data }) => {
|
||||
const { loggedIn } = useAuth();
|
||||
const [links, setLinks] = useState<EditableLink[]>(data ?? []);
|
||||
|
||||
console.log({ links });
|
||||
return loggedIn ? (
|
||||
<>
|
||||
<Analytics />
|
||||
<div>
|
||||
<Analytics clicks={links.reduce((acc, curr) => acc + curr.clicks, 0)} />
|
||||
<Editor links={links} setLinks={setLinks} />
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<LoginScreen />
|
||||
<Login />
|
||||
);
|
||||
};
|
||||
|
||||
export default function EditorPageWrapper({ data }: any): JSX.Element {
|
||||
export default function EditorPageWrapper(): JSX.Element {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<EditorPage data={data} />
|
||||
<EditorPage />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"greeting": "Hello from JSON"
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import React from "react";
|
||||
import { GetStaticProps } from "next";
|
||||
import GreetingJSON from "./hello-world.json";
|
||||
import { fetchExample } from "utils/api";
|
||||
|
||||
interface Props {
|
||||
greeting: string;
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<Props> = async () => {
|
||||
return {
|
||||
props: { greeting: GreetingJSON.greeting }, // will be passed to the page component as props
|
||||
// Next.js will attempt to re-generate the page:
|
||||
// - When a request comes in
|
||||
// - At most once every second
|
||||
revalidate: 1,
|
||||
};
|
||||
};
|
||||
|
||||
function HelloWorld({ greeting }: Props): JSX.Element {
|
||||
const [greetings, setGreetings] = React.useState([greeting]);
|
||||
|
||||
const getMoreGreetings = async () => {
|
||||
const greetingFromBackend = await fetchExample();
|
||||
setGreetings([
|
||||
...greetings,
|
||||
`"${greetingFromBackend}" --- ${new Date().toISOString()}`,
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "50px" }}>
|
||||
<button
|
||||
style={{ border: "2px solid black", padding: "5px" }}
|
||||
onClick={getMoreGreetings}
|
||||
>
|
||||
Get greeting from the server
|
||||
</button>
|
||||
<ol style={{ listStyleType: "decimal" }}>
|
||||
{greetings.map((greeting) => (
|
||||
<li key={greeting}>{greeting}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HelloWorld;
|
|
@ -1,29 +0,0 @@
|
|||
import React from "react";
|
||||
import { GetStaticProps } from "next";
|
||||
import { Links } from "components";
|
||||
|
||||
// TODO: change
|
||||
const API = "https://api.thedogapi.com/v1/breeds?limit=10&page=0";
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
// fetch data here
|
||||
const data = await fetch(API).then((res) => res.json());
|
||||
|
||||
return {
|
||||
props: { data }, // will be passed to the page component as props
|
||||
revalidate: 1,
|
||||
};
|
||||
};
|
||||
|
||||
const Home: React.FC = ({ data }: any) => {
|
||||
return (
|
||||
<Links
|
||||
links={data.map((dog: any) => ({
|
||||
title: dog.name,
|
||||
url: "https://www.google.com/",
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
|
@ -0,0 +1,5 @@
|
|||
RewriteEngine On
|
||||
|
||||
RewriteCond %{SCRIPT_FILENAME} !-d
|
||||
RewriteCond %{SCRIPT_FILENAME} !-f
|
||||
RewriteRule "^api/(.*)$" "http://corn-syrup:5730/$1" [P]
|
Binary file not shown.
Before Width: | Height: | Size: 96 KiB |
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 102 KiB |
Binary file not shown.
After Width: | Height: | Size: 189 KiB |
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
|
@ -0,0 +1,17 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* NextJS reset styles https://gist.github.com/dmurawsky/d45f068097d181c733a53687edce1919 */
|
||||
html,
|
||||
body,
|
||||
body > div:first-child,
|
||||
div#__next,
|
||||
div#__next > div {
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
purge: {
|
||||
content: [
|
||||
"../../backend/templates/template.html",
|
||||
],
|
||||
// These options are passed through directly to PurgeCSS
|
||||
options: {
|
||||
keyframes: true,
|
||||
fontFace: true,
|
||||
},
|
||||
},
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
fontFamily: {
|
||||
body: ["Karla", "sans-serif"],
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
analytics: {
|
||||
"view-icon": "#39e09b",
|
||||
"click-icon": "#8a86e5",
|
||||
},
|
||||
gray: {
|
||||
450: "#3d3b3c",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["Karla", "Helvetica", "Ubuntu", "sans-serif"],
|
||||
karla: ["Karla", "Verdana", "sans-serif"],
|
||||
},
|
||||
fontSize: {
|
||||
s: ".82rem",
|
||||
},
|
||||
screens: {
|
||||
"lt-lg": "992px",
|
||||
},
|
||||
flex: {
|
||||
analytics: "1 0 0",
|
||||
chevron: "0 0 16px",
|
||||
},
|
||||
borderRadius: {
|
||||
preview: "3.2rem",
|
||||
},
|
||||
borderWidth: {
|
||||
preview: "0.75rem",
|
||||
},
|
||||
minHeight: {
|
||||
link: "48px",
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {
|
||||
opacity: ["disabled"],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -4,14 +4,16 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: "Karla";
|
||||
src: local("Karla"),
|
||||
url("/fonts/Karla-VariableFont_wght.ttf") format("truetype");
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* NextJS reset styles https://gist.github.com/dmurawsky/d45f068097d181c733a53687edce1919 */
|
||||
html,
|
||||
body,
|
||||
body > div:first-child,
|
||||
div#__next,
|
||||
div#__next > div {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -13,10 +13,12 @@ module.exports = {
|
|||
},
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
fontFamily: {
|
||||
body: ["Karla", "sans-serif"],
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
analytics: {
|
||||
border: "#d7dce1",
|
||||
"view-icon": "#39e09b",
|
||||
"click-icon": "#8a86e5",
|
||||
},
|
||||
|
@ -38,19 +40,20 @@ module.exports = {
|
|||
analytics: "1 0 0",
|
||||
chevron: "0 0 16px",
|
||||
},
|
||||
},
|
||||
minWidth: {
|
||||
"9/10": "90%",
|
||||
},
|
||||
maxWidth: {
|
||||
"6/10": "60%",
|
||||
},
|
||||
container: {
|
||||
center: true,
|
||||
borderRadius: {
|
||||
preview: "3.2rem",
|
||||
},
|
||||
borderWidth: {
|
||||
preview: "0.75rem",
|
||||
},
|
||||
minHeight: {
|
||||
link: "48px",
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
extend: {
|
||||
opacity: ["disabled"],
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export async function fetchExample(): Promise<string> {
|
||||
const response = await fetch("/api");
|
||||
return await response.text();
|
||||
}
|
13
setup.sh
13
setup.sh
|
@ -3,6 +3,7 @@
|
|||
set -e
|
||||
|
||||
source ./common.sh
|
||||
MAIN_PATH=$(pwd)
|
||||
|
||||
function setup_frontend() {
|
||||
prefix_stdout_stderr "${PURPLE}frontend: ${NC}"
|
||||
|
@ -12,6 +13,10 @@ function setup_frontend() {
|
|||
echo "Installing dependencies..."
|
||||
npm i
|
||||
|
||||
echo "Building tailwind css file for links page"
|
||||
cd public
|
||||
NODE_ENV=production npx tailwindcss-cli@latest build index.in.css -o index.out.css
|
||||
|
||||
echo "Done!"
|
||||
}
|
||||
|
||||
|
@ -26,6 +31,9 @@ function setup_backend() {
|
|||
echo "Deleting sqlite database..."
|
||||
rm ./links.db || echo "Nothing to delete ¯\_(ツ)_/¯"
|
||||
|
||||
echo "Deleting .env file"
|
||||
rm ./.env || echo "No .env to delete"
|
||||
|
||||
echo "Creating new virtual environment..."
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
@ -36,6 +44,11 @@ function setup_backend() {
|
|||
echo "Creating a dummy sqlite database at 'backend/links.db'..."
|
||||
python setup_db.py
|
||||
|
||||
echo "Creating .env file"
|
||||
echo "PASSWORD=test" > .env
|
||||
echo "PORT=5000" >> .env
|
||||
echo "OUT_PATH=$MAIN_PATH/frontend/public/index.html" >> .env
|
||||
|
||||
echo "Done!"
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue