Compare commits
3 Commits
main
...
adi-next-e
Author | SHA1 | Date |
---|---|---|
Aditya Thakral | ae34744c47 | |
Aditya Thakral | 39692a0333 | |
Aditya Thakral | 5de03bf8d1 |
|
@ -4,7 +4,3 @@ password.txt
|
||||||
/.vs
|
/.vs
|
||||||
/.vscode
|
/.vscode
|
||||||
data.json
|
data.json
|
||||||
.env
|
|
||||||
build/
|
|
||||||
index.out.css
|
|
||||||
frontend/public/index.html
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
### 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
|
|
|
@ -1,28 +1,18 @@
|
||||||
from flask import Flask, request, jsonify, render_template
|
from flask import Flask, request, jsonify
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
import json
|
import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
import subprocess
|
||||||
|
|
||||||
DB_PATH = os.path.join(os.path.dirname(__file__), 'links.db')
|
DB_PATH = os.path.join(os.path.dirname(__file__), 'links.db')
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
auth = HTTPBasicAuth('CustomBasic')
|
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 = {
|
users = {
|
||||||
"admin": generate_password_hash(password),
|
"admin": generate_password_hash("test"),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_data_from_query(query):
|
def get_data_from_query(query):
|
||||||
|
@ -39,13 +29,11 @@ def get_data_from_query(query):
|
||||||
con.close()
|
con.close()
|
||||||
return links_list
|
return links_list
|
||||||
|
|
||||||
def regen_html(path):
|
def regen_JSON():
|
||||||
"""Gets links from DB and outputs them in HTML"""
|
"""Gets links from DB and outputs them in JSON"""
|
||||||
outfile = open(path, 'w')
|
|
||||||
links_list = get_data_from_query('SELECT url, name FROM links WHERE active=1 ORDER BY position')
|
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)
|
links_json = json.dumps(links_list, indent=4)
|
||||||
print(html, file=outfile)
|
return links_json
|
||||||
outfile.close()
|
|
||||||
|
|
||||||
@auth.verify_password
|
@auth.verify_password
|
||||||
def verify_password(username, password):
|
def verify_password(username, password):
|
||||||
|
@ -61,21 +49,19 @@ def get_links():
|
||||||
@app.route('/editor/links', methods = ['POST'])
|
@app.route('/editor/links', methods = ['POST'])
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
def update_links():
|
def update_links():
|
||||||
|
|
||||||
con = sqlite3.connect(DB_PATH)
|
con = sqlite3.connect(DB_PATH)
|
||||||
cur = con.cursor()
|
cur = con.cursor()
|
||||||
try:
|
try:
|
||||||
data = request.json['links']
|
cur.execute("begin")
|
||||||
|
cur.execute('DELETE FROM links')
|
||||||
|
|
||||||
|
links = []
|
||||||
|
data = request.json['links']
|
||||||
items = 'url', 'name', 'clicks', 'active'
|
items = 'url', 'name', 'clicks', 'active'
|
||||||
for i in range(len(data)):
|
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
|
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']
|
url = data[i]['url']
|
||||||
name = data[i]['name']
|
name = data[i]['name']
|
||||||
clicks = data[i]['clicks']
|
clicks = data[i]['clicks']
|
||||||
|
@ -88,7 +74,17 @@ def update_links():
|
||||||
cur.executemany('INSERT INTO links VALUES (?,?,?,?,?)', links)
|
cur.executemany('INSERT INTO links VALUES (?,?,?,?,?)', links)
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
regen_html(out_path)
|
|
||||||
|
frontend_path = os.environ.get('FRONTEND_PATH', None)
|
||||||
|
if frontend_path is None:
|
||||||
|
raise Exception('FRONTEND_PATH is not defined')
|
||||||
|
|
||||||
|
update_command = f"cd '{frontend_path}' && ./update.sh"
|
||||||
|
status = subprocess.call(update_command, shell=True)
|
||||||
|
|
||||||
|
if status != 0:
|
||||||
|
raise Exception(f"`{update_command}` exited with an error ({status})")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
cur.execute("rollback")
|
cur.execute("rollback")
|
||||||
con.close()
|
con.close()
|
||||||
|
@ -119,4 +115,4 @@ def update_clicks():
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(port=port, host="0.0.0.0")
|
app.run(debug=True)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
astroid==2.5.1
|
astroid==2.5.1
|
||||||
click==7.1.2
|
click==7.1.2
|
||||||
Flask==1.1.2
|
Flask==1.1.2
|
||||||
Flask-HTTPAuth==4.2.0
|
|
||||||
isort==5.7.0
|
isort==5.7.0
|
||||||
itsdangerous==1.1.0
|
itsdangerous==1.1.0
|
||||||
Jinja2==2.11.3
|
Jinja2==2.11.3
|
||||||
|
@ -9,9 +8,7 @@ lazy-object-proxy==1.5.2
|
||||||
MarkupSafe==1.1.1
|
MarkupSafe==1.1.1
|
||||||
mccabe==0.6.1
|
mccabe==0.6.1
|
||||||
pylint==2.7.2
|
pylint==2.7.2
|
||||||
python-dotenv==0.17.1
|
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
typed-ast==1.4.3
|
|
||||||
types-click==0.1.4
|
types-click==0.1.4
|
||||||
types-Flask==0.1.1
|
types-Flask==0.1.1
|
||||||
types-Jinja2==0.1.0
|
types-Jinja2==0.1.0
|
||||||
|
@ -20,3 +17,4 @@ types-typing-extensions==3.7.2
|
||||||
types-Werkzeug==0.1.1
|
types-Werkzeug==0.1.1
|
||||||
Werkzeug==1.0.1
|
Werkzeug==1.0.1
|
||||||
wrapt==1.12.1
|
wrapt==1.12.1
|
||||||
|
Flask-HTTPAuth==4.2.0
|
||||||
|
|
|
@ -7,11 +7,11 @@ con = sqlite3.connect(DB_PATH)
|
||||||
|
|
||||||
# array of links to store
|
# array of links to store
|
||||||
links = [
|
links = [
|
||||||
('http://csclub.uwaterloo.ca/','CS Club Website',0,0,1),
|
('http://csclub.uwaterloo.ca/','CS Club Website',3,0,1),
|
||||||
('https://www.instagram.com/uwcsclub/','Instagram',0,1,1),
|
('https://www.instagram.com/uwcsclub/','Instagram',4,1,1),
|
||||||
('https://www.facebook.com/uw.computerscienceclub','Facebook',0,2,1),
|
('https://www.facebook.com/uw.computerscienceclub','Facebook',5,2,1),
|
||||||
('http://twitch.tv/uwcsclub','Twitch',0,3,1),
|
('http://twitch.tv/uwcsclub','Twitch',6,3,1),
|
||||||
('http://bit.ly/uwcsclub-yt','YouTube',0,4,1),
|
('http://bit.ly/uwcsclub-yt','YouTube',7,4,1),
|
||||||
]
|
]
|
||||||
|
|
||||||
# SQLite setup
|
# SQLite setup
|
||||||
|
@ -23,9 +23,9 @@ if cur.fetchone():
|
||||||
raise Exception('Links table already exists.')
|
raise Exception('Links table already exists.')
|
||||||
else:
|
else:
|
||||||
cur.execute('''CREATE TABLE links (
|
cur.execute('''CREATE TABLE links (
|
||||||
url text NOT NULL,
|
url text NOT NULL,
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
clicks int NOT NULL,
|
clicks int NOT NULL,
|
||||||
position int NOT NULL UNIQUE,
|
position int NOT NULL UNIQUE,
|
||||||
active int NOT NULL,
|
active int NOT NULL,
|
||||||
UNIQUE(url, name)
|
UNIQUE(url, name)
|
||||||
|
@ -33,4 +33,4 @@ else:
|
||||||
cur.executemany('INSERT INTO links VALUES (?,?,?,?,?)', links)
|
cur.executemany('INSERT INTO links VALUES (?,?,?,?,?)', links)
|
||||||
con.commit()
|
con.commit()
|
||||||
|
|
||||||
con.close()
|
con.close()
|
|
@ -1,48 +0,0 @@
|
||||||
<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>
|
|
22
build.bash
22
build.bash
|
@ -1,22 +0,0 @@
|
||||||
#!/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
|
|
2
dev.sh
2
dev.sh
|
@ -18,7 +18,7 @@ function dev_backend() {
|
||||||
cd ./backend
|
cd ./backend
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
||||||
python main.py
|
FRONTEND_UPDATE_SCRIPT=$(realpath ./frontend/update.sh) python main.py
|
||||||
}
|
}
|
||||||
|
|
||||||
run_frontend_backend "dev_frontend" "dev_backend"
|
run_frontend_backend "dev_frontend" "dev_backend"
|
||||||
|
|
|
@ -1,22 +1,50 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
interface AnalyticsProps {
|
interface AnalyticsProps {
|
||||||
clicks: number;
|
clicks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Analytics: React.FC<AnalyticsProps> = ({ clicks }) => {
|
const Analytics: React.FC<AnalyticsProps> = ({ clicks }) => {
|
||||||
return (
|
return (
|
||||||
<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-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 items-center">
|
<div className="w-full h-full flex-analytics flex flex-row justify-start items-center">
|
||||||
<span className="mr-4 font-bold">Lifetime Analytics:</span>
|
<span className="mr-4 font-bold">Lifetime Analytics:</span>
|
||||||
<div className="mr-8 flex justify-center items-center">
|
<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="h-2 w-2 mr-2 rounded bg-analytics-click-icon"></div>
|
||||||
<div className="flex text-xs lt-lg:text-base font-normal">
|
<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="whitespace-pre">Clicks: </p>
|
||||||
<p className="font-bold lt-lg:font-normal">{clicks || "-"}</p>
|
<p className="font-bold lt-lg:font-normal">{clicks || "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-full h-full flex-chevron flex flex-row justify-center items-center">
|
||||||
|
<Chevron />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,7 +44,7 @@ const Link: React.FC<LinkProps> = ({ index, link, onChange, onDelete }) => {
|
||||||
<Draggable key={index} draggableId={index.toString()} index={index}>
|
<Draggable key={index} draggableId={index.toString()} index={index}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div {...provided.draggableProps} ref={provided.innerRef}>
|
<div {...provided.draggableProps} ref={provided.innerRef}>
|
||||||
<div className="py-2 flex rounded-md md:container">
|
<div className="p-2 container flex">
|
||||||
<div
|
<div
|
||||||
className="flex justify-center w-1/12 flex items-center border-r bg-white rounded-sm shadow-sm"
|
className="flex justify-center w-1/12 flex items-center border-r bg-white rounded-sm shadow-sm"
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
|
@ -84,9 +84,6 @@ const Link: React.FC<LinkProps> = ({ index, link, onChange, onDelete }) => {
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center px-2 pb-2">
|
|
||||||
{`Clicks: ${link.clicks}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -57,8 +57,8 @@ const Editor: React.FC<EditorProps> = ({ links, setLinks }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedLinks = await res.json();
|
const updatedLinks = await res.json();
|
||||||
setLinks(updatedLinks);
|
|
||||||
|
|
||||||
|
setLinks(updatedLinks);
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,75 +67,70 @@ const Editor: React.FC<EditorProps> = ({ links, setLinks }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-gray-100 md:flex-row">
|
<div className="flex">
|
||||||
<div className="space-y-4 md:w-3/5 md:border-r md:border-gray-300">
|
<div className="space-y-4 bg-gray-100 w-1/2 p-2">
|
||||||
<div className="m-8">
|
{/* 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}
|
||||||
|
>
|
||||||
|
{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-16" />
|
||||||
|
<div className="flex">
|
||||||
<button
|
<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"
|
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={handleOnClickAdd}
|
onClick={onSubmit}
|
||||||
|
disabled={isSaving || equal(editableLinks, links)}
|
||||||
>
|
>
|
||||||
Add New Link
|
{isSaving ? "Saving ..." : "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={isSaving || equal(editableLinks, links)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<div className="mb-8 md:none" />
|
<Preview links={editableLinks.filter((link) => link.active)} />
|
||||||
<div className="flex m-14 justify-center md:w-2/5">
|
|
||||||
<Preview links={editableLinks.filter((link) => link.active)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,23 +6,39 @@ export interface Link {
|
||||||
}
|
}
|
||||||
interface Props {
|
interface Props {
|
||||||
links: Link[];
|
links: Link[];
|
||||||
|
logClicks?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Links: React.FC<Props> = ({ links }) => {
|
export const Links: React.FC<Props> = ({ 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 }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-s flex flex-col items-center w-full top-6 font-karla">
|
<div
|
||||||
<img className="mb-3" src="images/csc-logo.png" alt="CSC Logo" width="100px" />
|
className={`text-s flex flex-col items-center w-full top-6 font-karla`}
|
||||||
|
>
|
||||||
|
<img className="mb-3" src="csc_logo.png" alt="CSC Logo" width="100px" />
|
||||||
<h1 className="font-bold">@uwcsclub</h1>
|
<h1 className="font-bold">@uwcsclub</h1>
|
||||||
<ul className="flex flex-col my-6 w-full">
|
<ul className="flex flex-col my-6 w-full">
|
||||||
{links.map(({ name, url }, i) => (
|
{links.map(({ name, url }, i) => (
|
||||||
<li key={i} className="w-full flex justify-center">
|
<li key={i} className="w-full contents">
|
||||||
<a
|
<a
|
||||||
className="btn bg-gray-450 p-3 text-white font-karla font-bold text-center self-center my-1.5
|
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-300 ease-in-out
|
hover:bg-white hover:text-black border-2 border-gray-800 transition duration-200 ease-in-out
|
||||||
w-11/12 sm:w-4/12 min-h-link"
|
w-11/12 sm:w-4/12"
|
||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
|
onClick={() => handleClick(name, url)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -19,7 +19,13 @@ export const AuthProvider: React.FC = ({ children }) => {
|
||||||
const logout = () => setHeaders(undefined);
|
const logout = () => setHeaders(undefined);
|
||||||
|
|
||||||
const login = async (password: string): Promise<boolean> => {
|
const login = async (password: string): Promise<boolean> => {
|
||||||
const username = "admin";
|
const username = process.env.NEXT_PUBLIC_EDITOR_USERNAME;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
throw new Error(
|
||||||
|
"Missing NEXT_PUBLIC_EDITOR_USERNAME environment variable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const newHeaders = {
|
const newHeaders = {
|
||||||
Authorization: `CustomBasic ${btoa(`${username}:${password}`)}`,
|
Authorization: `CustomBasic ${btoa(`${username}:${password}`)}`,
|
||||||
|
|
|
@ -37,7 +37,7 @@ const LoginBox: React.FC = () => {
|
||||||
<div className="flex flex-col justify-center items-center space-y-10">
|
<div className="flex flex-col justify-center items-center space-y-10">
|
||||||
<div className="flex flex-row justify-center items-center space-x-5">
|
<div className="flex flex-row justify-center items-center space-x-5">
|
||||||
<img
|
<img
|
||||||
src="images/csc-logo-trans.png"
|
src="csc-logo-trans.png"
|
||||||
height={80}
|
height={80}
|
||||||
width={80}
|
width={80}
|
||||||
alt="CSC Logo"
|
alt="CSC Logo"
|
||||||
|
|
|
@ -9,9 +9,6 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
padding-right: 17px; /* Increase/decrease this value for cross-browser compatibility */
|
padding-right: 17px; /* Increase/decrease this value for cross-browser compatibility */
|
||||||
padding-left: 7px;
|
padding-left: 5px;
|
||||||
box-sizing: content-box; /* So the width will be 100% + 17px */
|
box-sizing: content-box; /* So the width will be 100% + 17px */
|
||||||
}
|
}
|
||||||
.child a {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const { PHASE_DEVELOPMENT_SERVER } = require("next/constants");
|
module.exports = {
|
||||||
|
|
||||||
const devConfig = {
|
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -19,11 +15,3 @@ const devConfig = {
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prodConfig = {
|
|
||||||
basePath: "/links",
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
module.exports = (phase) =>
|
|
||||||
phase === PHASE_DEVELOPMENT_SERVER ? devConfig : prodConfig;
|
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build && next export",
|
||||||
"export": "next export",
|
|
||||||
"type-check": "tsc",
|
"type-check": "tsc",
|
||||||
"format": "prettier --write './**/*'",
|
"format": "prettier --write './**/*'",
|
||||||
"format:check": "prettier --check './**/*'",
|
"format:check": "prettier --check './**/*'",
|
||||||
|
|
|
@ -22,10 +22,10 @@ const EditorPage: React.FC = () => {
|
||||||
}, [loggedIn, headers]);
|
}, [loggedIn, headers]);
|
||||||
|
|
||||||
return loggedIn ? (
|
return loggedIn ? (
|
||||||
<div>
|
<>
|
||||||
<Analytics clicks={links.reduce((acc, curr) => acc + curr.clicks, 0)} />
|
<Analytics clicks={links.reduce((acc, curr) => acc + curr.clicks, 0)} />
|
||||||
<Editor links={links} setLinks={setLinks} />
|
<Editor links={links} setLinks={setLinks} />
|
||||||
</div>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Login />
|
<Login />
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from "react";
|
||||||
|
import { GetStaticProps } from "next";
|
||||||
|
import { Link, Links } from "components/Links";
|
||||||
|
|
||||||
|
export const getStaticProps: GetStaticProps<Props> = async () => {
|
||||||
|
const res = await fetch(`http://${process.env.SERVER_URL}/links`);
|
||||||
|
const links = await res.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { links },
|
||||||
|
revalidate: 60,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
links: Link[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Home: React.FC<Props> = ({ links }) => {
|
||||||
|
return <Links links={links} logClicks={true} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
|
@ -1,5 +0,0 @@
|
||||||
RewriteEngine On
|
|
||||||
|
|
||||||
RewriteCond %{SCRIPT_FILENAME} !-d
|
|
||||||
RewriteCond %{SCRIPT_FILENAME} !-f
|
|
||||||
RewriteRule "^api/(.*)$" "http://corn-syrup:5730/$1" [P]
|
|
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
Binary file not shown.
After 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: 189 KiB |
Binary file not shown.
Before Width: | Height: | Size: 47 KiB |
|
@ -1,17 +0,0 @@
|
||||||
@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%;
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
// 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"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -4,16 +4,14 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Karla";
|
||||||
|
src: local("Karla"),
|
||||||
|
url("/fonts/Karla-VariableFont_wght.ttf") format("truetype");
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
button {
|
button {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NextJS reset styles https://gist.github.com/dmurawsky/d45f068097d181c733a53687edce1919 */
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
body > div:first-child,
|
|
||||||
div#__next,
|
|
||||||
div#__next > div {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ module.exports = {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
analytics: {
|
analytics: {
|
||||||
|
border: "#d7dce1",
|
||||||
"view-icon": "#39e09b",
|
"view-icon": "#39e09b",
|
||||||
"click-icon": "#8a86e5",
|
"click-icon": "#8a86e5",
|
||||||
},
|
},
|
||||||
|
@ -46,9 +47,9 @@ module.exports = {
|
||||||
borderWidth: {
|
borderWidth: {
|
||||||
preview: "0.75rem",
|
preview: "0.75rem",
|
||||||
},
|
},
|
||||||
minHeight: {
|
},
|
||||||
link: "48px",
|
container: {
|
||||||
},
|
center: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
npm run build
|
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting the python server..."
|
||||||
|
|
||||||
|
pushd backend
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
FRONTEND_PATH=$(realpath ../frontend) python main.py &
|
||||||
|
pid_backend=$!
|
||||||
|
|
||||||
|
trap_ctrlc() {
|
||||||
|
kill $pid_backend
|
||||||
|
}
|
||||||
|
|
||||||
|
trap trap_ctrlc INT
|
||||||
|
|
||||||
|
popd
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo "Building frontend..."
|
||||||
|
|
||||||
|
pushd frontend
|
||||||
|
npm run build
|
||||||
|
popd
|
||||||
|
|
||||||
|
echo "Done!"
|
||||||
|
|
||||||
|
wait
|
13
setup.sh
13
setup.sh
|
@ -3,7 +3,6 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
source ./common.sh
|
source ./common.sh
|
||||||
MAIN_PATH=$(pwd)
|
|
||||||
|
|
||||||
function setup_frontend() {
|
function setup_frontend() {
|
||||||
prefix_stdout_stderr "${PURPLE}frontend: ${NC}"
|
prefix_stdout_stderr "${PURPLE}frontend: ${NC}"
|
||||||
|
@ -13,10 +12,6 @@ function setup_frontend() {
|
||||||
echo "Installing dependencies..."
|
echo "Installing dependencies..."
|
||||||
npm i
|
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!"
|
echo "Done!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,9 +26,6 @@ function setup_backend() {
|
||||||
echo "Deleting sqlite database..."
|
echo "Deleting sqlite database..."
|
||||||
rm ./links.db || echo "Nothing to delete ¯\_(ツ)_/¯"
|
rm ./links.db || echo "Nothing to delete ¯\_(ツ)_/¯"
|
||||||
|
|
||||||
echo "Deleting .env file"
|
|
||||||
rm ./.env || echo "No .env to delete"
|
|
||||||
|
|
||||||
echo "Creating new virtual environment..."
|
echo "Creating new virtual environment..."
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
@ -44,11 +36,6 @@ function setup_backend() {
|
||||||
echo "Creating a dummy sqlite database at 'backend/links.db'..."
|
echo "Creating a dummy sqlite database at 'backend/links.db'..."
|
||||||
python setup_db.py
|
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!"
|
echo "Done!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue