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 + + + + + + + +
+
+ CSC Logo +

@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 (
- CSC Logo + CSC Logo

@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 = () => {
    - CSC Logo { 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;