diff --git a/.gitignore b/.gitignore
index 358aebf..35264fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,6 @@ password.txt
/.vs
/.vscode
data.json
+.env
+build/
+index.out.css
diff --git a/README-deploy.md b/README-deploy.md
new file mode 100644
index 0000000..0d5d470
--- /dev/null
+++ b/README-deploy.md
@@ -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
diff --git a/backend/main.py b/backend/main.py
index 9a6d74b..30e461c 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1,16 +1,28 @@
-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('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 get_data_from_query(query):
@@ -27,11 +39,13 @@ def get_data_from_query(query):
con.close()
return links_list
-def regen_JSON():
- """Gets links from DB and outputs them in JSON"""
+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')
- links_json = json.dumps(links_list, indent=4)
- return links_json
+ html = render_template('template.html', links_list=links_list)
+ print(html, file=outfile)
+ outfile.close()
@auth.verify_password
def verify_password(username, password):
@@ -47,19 +61,21 @@ def get_links():
@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', 'active'
for i in range(len(data)):
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']
@@ -72,12 +88,7 @@ def update_links():
cur.executemany('INSERT INTO links VALUES (?,?,?,?,?)', links)
con.commit()
con.close()
- data = regen_JSON()
- # TODO: Trigger a rebuild of the frontend
- outfile = open('data.json', 'w')
- print(data, file=outfile)
- outfile.close()
-
+ regen_html(out_path)
except Exception as e:
cur.execute("rollback")
con.close()
@@ -108,4 +119,4 @@ def update_clicks():
return 'ok'
if __name__ == "__main__":
- app.run(debug=True)
+ app.run(port=port, host="0.0.0.0")
diff --git a/backend/requirements.txt b/backend/requirements.txt
index db479c9..41bad33 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -1,14 +1,18 @@
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
lazy-object-proxy==1.5.2
MarkupSafe==1.1.1
mccabe==0.6.1
+pkg-resources==0.0.0
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 +21,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
diff --git a/backend/setup_db.py b/backend/setup_db.py
index dda91c4..0fb91c5 100644
--- a/backend/setup_db.py
+++ b/backend/setup_db.py
@@ -7,11 +7,11 @@ con = sqlite3.connect(DB_PATH)
# array of links to store
links = [
- ('http://csclub.uwaterloo.ca/','CS Club Website',3,0,1),
- ('https://www.instagram.com/uwcsclub/','Instagram',4,1,1),
- ('https://www.facebook.com/uw.computerscienceclub','Facebook',5,2,1),
- ('http://twitch.tv/uwcsclub','Twitch',6,3,1),
- ('http://bit.ly/uwcsclub-yt','YouTube',7,4,1),
+ ('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,9 +23,9 @@ 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)
@@ -33,4 +33,4 @@ else:
cur.executemany('INSERT INTO links VALUES (?,?,?,?,?)', links)
con.commit()
-con.close()
\ No newline at end of file
+con.close()
diff --git a/backend/templates/template.html b/backend/templates/template.html
new file mode 100644
index 0000000..b7f373a
--- /dev/null
+++ b/backend/templates/template.html
@@ -0,0 +1,48 @@
+
+
+
+
+ @uwcsclub | LinkList
+
+
+
+
+
+
+
+
+
+

+
@uwcsclub
+
+
+
+
+
diff --git a/build.bash b/build.bash
new file mode 100755
index 0000000..d9e996d
--- /dev/null
+++ b/build.bash
@@ -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
diff --git a/frontend/components/Editor/Link.tsx b/frontend/components/Editor/Link.tsx
index d39b681..49fbf15 100644
--- a/frontend/components/Editor/Link.tsx
+++ b/frontend/components/Editor/Link.tsx
@@ -84,6 +84,9 @@ const Link: React.FC = ({ index, link, onChange, onDelete }) => {
+
+ {`Clicks: ${link.clicks}`}
+
diff --git a/frontend/components/Editor/index.tsx b/frontend/components/Editor/index.tsx
index c730222..5180c1b 100644
--- a/frontend/components/Editor/index.tsx
+++ b/frontend/components/Editor/index.tsx
@@ -16,6 +16,7 @@ const Editor: React.FC = ({ links, setLinks }) => {
const { displayDragDrop } = useDragDrop();
const { headers } = useAuth();
const [editableLinks, setEditableLinks] = useState(links);
+ const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
setEditableLinks(links);
@@ -44,7 +45,9 @@ const Editor: React.FC = ({ links, setLinks }) => {
]);
const onSubmit = async () => {
- const res = await fetch("/api/editor/links", {
+ setIsSaving(true);
+
+ const res = await fetch("api/editor/links", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -52,8 +55,11 @@ const Editor: React.FC = ({ links, setLinks }) => {
},
body: JSON.stringify({ links: editableLinks }),
});
+
const updatedLinks = await res.json();
setLinks(updatedLinks);
+
+ setIsSaving(false);
};
const onCancel = () => {
@@ -111,7 +117,7 @@ const Editor: React.FC = ({ links, setLinks }) => {
@@ -119,7 +125,7 @@ const Editor: React.FC = ({ links, setLinks }) => {
diff --git a/frontend/components/Links/index.tsx b/frontend/components/Links/index.tsx
index b9f3f9f..2db81a7 100644
--- a/frontend/components/Links/index.tsx
+++ b/frontend/components/Links/index.tsx
@@ -6,25 +6,12 @@ export interface Link {
}
interface Props {
links: Link[];
- logClicks?: boolean;
}
-export const Links: React.FC = ({ links, logClicks = false }) => {
- const handleClick = (name: string, url: string) => {
- if (logClicks) {
- fetch("/api/clicks", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ name, url }),
- });
- }
- };
-
+export const Links: React.FC = ({ links }) => {
return (
-

+
@uwcsclub
{links.map(({ name, url }, i) => (
@@ -36,7 +23,6 @@ export const Links: React.FC = ({ links, logClicks = false }) => {
href={url}
target="_blank"
rel="noreferrer"
- onClick={() => handleClick(name, url)}
>
{name}
diff --git a/frontend/components/Login/AuthContext.tsx b/frontend/components/Login/AuthContext.tsx
index d85541b..f7dca10 100644
--- a/frontend/components/Login/AuthContext.tsx
+++ b/frontend/components/Login/AuthContext.tsx
@@ -19,19 +19,13 @@ export const AuthProvider: React.FC = ({ children }) => {
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 username = "admin";
const newHeaders = {
Authorization: `CustomBasic ${btoa(`${username}:${password}`)}`,
};
- const res = await fetch("/api/editor/links", { headers: newHeaders });
+ const res = await fetch("api/editor/links", { headers: newHeaders });
if (res.status === 200) {
setHeaders(newHeaders);
diff --git a/frontend/components/Login/index.tsx b/frontend/components/Login/index.tsx
index 53137fa..b70cebc 100644
--- a/frontend/components/Login/index.tsx
+++ b/frontend/components/Login/index.tsx
@@ -1,6 +1,5 @@
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 = () => {
@@ -37,8 +36,8 @@ const LoginBox: React.FC = () => {
-
{
async function fetchLinks() {
if (!loggedIn) return;
- const res = await fetch("/api/editor/links", { headers });
+ const res = await fetch("api/editor/links", { headers });
const links = await res.json();
setLinks(links);
}
diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx
deleted file mode 100644
index 2f157d1..0000000
--- a/frontend/pages/index.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from "react";
-import { GetStaticProps } from "next";
-import { Link, Links } from "components/Links";
-
-export const getStaticProps: GetStaticProps = async () => {
- const res = await fetch(`${process.env.DEV_URL}/links`);
- const links = await res.json();
-
- return {
- props: { links },
- revalidate: 60,
- };
-};
-
-interface Props {
- links: Link[];
-}
-
-const Home: React.FC = ({ links }) => {
- return ;
-};
-
-export default Home;
diff --git a/frontend/public/.htaccess b/frontend/public/.htaccess
new file mode 100644
index 0000000..f2b8153
--- /dev/null
+++ b/frontend/public/.htaccess
@@ -0,0 +1,5 @@
+RewriteEngine On
+
+RewriteCond %{SCRIPT_FILENAME} !-d
+RewriteCond %{SCRIPT_FILENAME} !-f
+RewriteRule "^api/(.*)$" "http://corn-syrup:5730/$1" [P]
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
deleted file mode 100644
index 4965832..0000000
Binary files a/frontend/public/favicon.ico and /dev/null differ
diff --git a/frontend/public/fonts/Karla-Bold.ttf b/frontend/public/fonts/Karla-Bold.ttf
deleted file mode 100644
index c58a746..0000000
Binary files a/frontend/public/fonts/Karla-Bold.ttf and /dev/null differ
diff --git a/frontend/public/fonts/Karla-Regular.ttf b/frontend/public/fonts/Karla-Regular.ttf
deleted file mode 100644
index a08cc9b..0000000
Binary files a/frontend/public/fonts/Karla-Regular.ttf and /dev/null differ
diff --git a/frontend/public/fonts/Karla-SemiBold.ttf b/frontend/public/fonts/Karla-SemiBold.ttf
deleted file mode 100644
index 970f34c..0000000
Binary files a/frontend/public/fonts/Karla-SemiBold.ttf and /dev/null differ
diff --git a/frontend/public/fonts/Karla-VariableFont_wght.ttf b/frontend/public/fonts/Karla-VariableFont_wght.ttf
deleted file mode 100644
index 172b500..0000000
Binary files a/frontend/public/fonts/Karla-VariableFont_wght.ttf and /dev/null differ
diff --git a/frontend/public/images/csc-logo-fb-trans.png b/frontend/public/images/csc-logo-trans.png
similarity index 100%
rename from frontend/public/images/csc-logo-fb-trans.png
rename to frontend/public/images/csc-logo-trans.png
diff --git a/frontend/public/csc_logo.png b/frontend/public/images/csc-logo.png
similarity index 100%
rename from frontend/public/csc_logo.png
rename to frontend/public/images/csc-logo.png
diff --git a/frontend/public/index.in.css b/frontend/public/index.in.css
new file mode 100644
index 0000000..8941564
--- /dev/null
+++ b/frontend/public/index.in.css
@@ -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%;
+}
diff --git a/frontend/public/tailwind.config.js b/frontend/public/tailwind.config.js
new file mode 100644
index 0000000..67ebbce
--- /dev/null
+++ b/frontend/public/tailwind.config.js
@@ -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"],
+ },
+ },
+};
diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg
deleted file mode 100644
index fbf0e25..0000000
--- a/frontend/public/vercel.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css
index 7fceea6..c7902ae 100644
--- a/frontend/styles/globals.css
+++ b/frontend/styles/globals.css
@@ -4,13 +4,6 @@
@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;