Compare commits

..

3 Commits

Author SHA1 Message Date
Aditya Thakral ae34744c47 Add isSaving to editor 2021-04-06 01:16:13 -04:00
Aditya Thakral 39692a0333 Regenerate website on update 2021-04-06 01:05:10 -04:00
Aditya Thakral 5de03bf8d1 Use relative paths instead of absolute paths 2021-04-06 00:13:02 -04:00
38 changed files with 235 additions and 337 deletions

4
.gitignore vendored
View File

@ -4,7 +4,3 @@ password.txt
/.vs /.vs
/.vscode /.vscode
data.json data.json
.env
build/
index.out.css
frontend/public/index.html

View File

@ -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

View File

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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(&quot;{{ link['name'] }}&quot;, &quot;{{ link['url'] }}&quot;)"
>
{{ link["name"] }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</body>
</html>

View File

@ -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
View File

@ -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"

View File

@ -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>
); );
}; };

View File

@ -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>

View File

@ -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>
); );
}; };

View File

@ -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>

View File

@ -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}`)}`,

View File

@ -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"

View File

@ -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%;
}

View File

@ -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;

View File

@ -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 './**/*'",

View File

@ -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 />
); );

23
frontend/pages/index.tsx Normal file
View File

@ -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;

View File

@ -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

BIN
frontend/public/favicon.ico Normal file

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

View File

@ -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%;
}

View File

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

View File

@ -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

View File

@ -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%;
}

View File

@ -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: {

3
frontend/update.sh Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
npm run build

31
prod.sh Executable file
View File

@ -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

View File

@ -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!"
} }