Merge pull request #792 from thedevs-network/develop

Add themes, and custom alphabet and trust proxy configuration
This commit is contained in:
Pouria Ezzati 2025-01-17 10:54:35 +03:30 committed by GitHub
commit 1cc7a80408
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 223 additions and 84 deletions

View File

@ -26,6 +26,15 @@ DB_POOL_MAX=10
# Optional - Generated link length # Optional - Generated link length
LINK_LENGTH=6 LINK_LENGTH=6
# Optional - Alphabet used to generate custom addresses
# Default value omits o, O, 0, i, I, l, 1, and j to avoid confusion when reading the URL
LINK_CUSTOM_ALPHABET=abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789
# Optional - Tells the app that it's running behind a proxy server
# and that it should get the IP address from that proxy server
# if you're not using a proxy server then set this to false, otherwise users can override their IP address
TRUST_PROXY=true
# Optional - Redis host and port # Optional - Redis host and port
REDIS_ENABLED=false REDIS_ENABLED=false
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1

View File

@ -20,6 +20,7 @@
- [Docker](#docker) - [Docker](#docker)
- [API](#api) - [API](#api)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Themes and customizations](#themes-and-customizations)
- [Browser extensions](#browser-extensions) - [Browser extensions](#browser-extensions)
- [Videos](#videos) - [Videos](#videos)
- [Integrations](#integrations) - [Integrations](#integrations)
@ -93,8 +94,10 @@ All variables are optional except `JWT_SECRET` which is required on production.
| `SITE_NAME` | Name of the website | `Kutt` | `Your Site` | | `SITE_NAME` | Name of the website | `Kutt` | `Your Site` |
| `DEFAULT_DOMAIN` | The domain address that this app runs on | `localhost:3000` | `yoursite.com` | | `DEFAULT_DOMAIN` | The domain address that this app runs on | `localhost:3000` | `yoursite.com` |
| `LINK_LENGTH` | The length of of shortened address | `6` | `5` | | `LINK_LENGTH` | The length of of shortened address | `6` | `5` |
| `LINK_CUSTOM_ALPHABET` | Alphabet used to generate custom addresses. Default value omits o, O, 0, i, I, l, 1, and j to avoid confusion when reading the URL. | (abcd..789) | `abcABC^&*()@` |
| `DISALLOW_REGISTRATION` | Disable registration. Note that if `MAIL_ENABLED` is set to false, then the registration would still be disabled since it relies on emails to sign up users. | `true` | `false` | | `DISALLOW_REGISTRATION` | Disable registration. Note that if `MAIL_ENABLED` is set to false, then the registration would still be disabled since it relies on emails to sign up users. | `true` | `false` |
| `DISALLOW_ANONYMOUS_LINKS` | Disable anonymous link creation | `true` | `false` | | `DISALLOW_ANONYMOUS_LINKS` | Disable anonymous link creation | `true` | `false` |
| `TRUST_PROXY` | If the app is running behind a proxy server like NGINX or Cloudflare and that it should get the IP address from that proxy server. If you're not using a proxy server then set this to false, otherwise users can override their IP address. | `true` | `false` |
| `DB_CLIENT` | Which database client to use. Supported clients: `pg` or `pg-native` for Postgres, `mysql2` for MySQL or MariaDB, `sqlite3` and `better-sqlite3` for SQLite. NOTE: `pg-native` and `better-sqlite3` are not installed by default, use `npm` to install them before use. | `sqlite3` | `pg` | | `DB_CLIENT` | Which database client to use. Supported clients: `pg` or `pg-native` for Postgres, `mysql2` for MySQL or MariaDB, `sqlite3` and `better-sqlite3` for SQLite. NOTE: `pg-native` and `better-sqlite3` are not installed by default, use `npm` to install them before use. | `sqlite3` | `pg` |
| `DB_HOST` | Database connection host. Only if you use Postgres or MySQL. | `localhost` | `your-db-host.com` | | `DB_HOST` | Database connection host. Only if you use Postgres or MySQL. | `localhost` | `your-db-host.com` |
| `DB_PORT` | Database port. Only if you use Postgres or MySQL. | `5432` (Postgres) | `3306` (MySQL) | | `DB_PORT` | Database port. Only if you use Postgres or MySQL. | `5432` (Postgres) | `3306` (MySQL) |
@ -118,10 +121,77 @@ All variables are optional except `JWT_SECRET` which is required on production.
| `MAIL_PORT` | Email server port | `587` | `465` (SSL) | | `MAIL_PORT` | Email server port | `587` | `465` (SSL) |
| `MAIL_USER` | Email server user | - | `myuser` | | `MAIL_USER` | Email server user | - | `myuser` |
| `MAIL_PASSWORD` | Email server password for the user | - | `mypassword` | | `MAIL_PASSWORD` | Email server password for the user | - | `mypassword` |
| `MAIL_FROM` | Email address to send the user from | - | `some.address@yoursite.com` | | `MAIL_FROM` | Email address to send the user from | - | `example@yoursite.com` |
| `MAIL_SECURE` | Whether use SSL for the email server connection | `false` | `true` | | `MAIL_SECURE` | Whether use SSL for the email server connection | `false` | `true` |
| `REPORT_EMAIL` | The email address that will receive submitted reports | - | `some.address@yoursite.com` | | `REPORT_EMAIL` | The email address that will receive submitted reports | - | `example@yoursite.com` |
| `CONTACT_EMAIL` | The support email address to show on the app | - | `some.address@yoursite.com` | | `CONTACT_EMAIL` | The support email address to show on the app | - | `example@yoursite.com` |
## Themes and customizations
You can add styles, change images, or render custom HTML. Place your content inside the [`/custom`](./custom) folder according to below instructions.
#### How it works:
The structure of the custom folder is like this:
```
custom/
├─ css/
│ ├─ custom1.css
│ ├─ custom2.css
│ ├─ ...
├─ images/
│ ├─ logo.png
│ ├─ favicon.ico
│ ├─ ...
├─ views/
│ ├─ partials/
│ │ ├─ footer.hbs
│ ├─ 404.hbs
│ ├─ ...
```
- **css**: Put your CSS style files here. ([View example →](https://github.com/thedevs-network/kutt-customizations/tree/main/themes/crimson/css))
- You can put as many style files as you want: `custom1.css`, `custom2.css`, etc.
- If you name your style file `styles.css`, it will replace Kutt's original `styles.css` file.
- Each file will be accessible by `<your-site.com>/css/<file>.css`
- **images**: Put your images here. ([View example →](https://github.com/thedevs-network/kutt-customizations/tree/main/themes/crimson/images))
- Name them just like the files inside the [`/static/images/`](./static/images) folder to replace Kutt's original images.
- Each image will be accessible by `<your-site.com>/images/<image>.<image-format>`
- **views**: Custom HTML templates to render. ([View example →](https://github.com/thedevs-network/kutt-customizations/tree/main/themes/crimson/views))
- It should follow the same file naming and folder structure as [`/server/views`](./server/views)
- Although we try to keep the original file names unchanged, be aware that new changes on Kutt might break your custom views.
#### Example theme: Crimson
This is an example and official theme. Crimson includes custom styles, images, and views.
[Get Crimson theme →](https://github.com/thedevs-network/kutt-customizations/tree/main/themes/crimson)
[View list of themes and customizations →](https://github.com/thedevs-network/kutt-customizations)
| Homepage | Admin page | Login/signup |
| -------- | ---------- | ------------ |
| ![crimson-homepage](https://github.com/user-attachments/assets/b74fab78-5e80-4f57-8425-f0cc73e9c68d) | ![crimson-admin](https://github.com/user-attachments/assets/a75d2430-8074-4ce4-93ec-d8bdfd75d917) | ![crimson-login-signup ](https://github.com/user-attachments/assets/b915eb77-3d66-4407-8e5d-b556f80ff453)
#### Usage with Docker:
If you're building the image locally, then the `/custom` folder should already be included in your app.
If you're pulling the official image, make sure `/kutt/custom` volume is mounted or you have access to it. [View Docker compose example →](https://github.com/thedevs-network/kutt/blob/main/docker-compose.yml#L7)
Then, move your files to that volume. You can do it with this Docker command:
```sh
docker cp <path-to-custom-folder> <kutt-container-name>:/kutt
```
For example:
```sh
docker cp custom kutt-server-1:/kutt
```
## Browser extensions ## Browser extensions

3
custom/.gitkeep Normal file
View File

@ -0,0 +1,3 @@
# keep this folder in git
# put supported customization files for styles and such
# if you're using docker make sure to mount this folder

View File

@ -2,6 +2,8 @@ services:
server: server:
build: build:
context: . context: .
volumes:
- custom:/kutt/custom
environment: environment:
DB_CLIENT: mysql2 DB_CLIENT: mysql2
DB_HOST: mariadb DB_HOST: mariadb
@ -39,4 +41,5 @@ services:
expose: expose:
- 6379 - 6379
volumes: volumes:
db_data_mariadb: db_data_mariadb:
custom:

View File

@ -2,9 +2,12 @@ services:
server: server:
build: build:
context: . context: .
volumes:
- custom:/kutt/custom
environment: environment:
DB_CLIENT: pg DB_CLIENT: pg
DB_HOST: postgres DB_HOST: postgres
DB_PORT: 5432
REDIS_ENABLED: true REDIS_ENABLED: true
REDIS_HOST: redis REDIS_HOST: redis
REDIS_PORT: 6379 REDIS_PORT: 6379
@ -37,4 +40,5 @@ services:
expose: expose:
- 6379 - 6379
volumes: volumes:
db_data_pg: db_data_pg:
custom:

View File

@ -3,7 +3,8 @@ services:
build: build:
context: . context: .
volumes: volumes:
- db-data:/var/lib/kutt - db_data_sqlite:/var/lib/kutt
- custom:/kutt/custom
environment: environment:
DB_FILENAME: "/var/lib/kutt/data.sqlite" DB_FILENAME: "/var/lib/kutt/data.sqlite"
REDIS_ENABLED: true REDIS_ENABLED: true
@ -20,4 +21,5 @@ services:
expose: expose:
- 6379 - 6379
volumes: volumes:
db-data: db_data_sqlite:
custom:

View File

@ -4,9 +4,11 @@ services:
context: . context: .
volumes: volumes:
- db_data_sqlite:/var/lib/kutt - db_data_sqlite:/var/lib/kutt
- custom:/kutt/custom
environment: environment:
DB_FILENAME: "/var/lib/kutt/data.sqlite" DB_FILENAME: "/var/lib/kutt/data.sqlite"
ports: ports:
- 3000:3000 - 3000:3000
volumes: volumes:
db_data_sqlite: db_data_sqlite:
custom:

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "kutt", "name": "kutt",
"version": "3.0.4", "version": "3.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "kutt", "name": "kutt",
"version": "3.0.4", "version": "3.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",

View File

@ -1,10 +1,10 @@
{ {
"name": "kutt", "name": "kutt",
"version": "3.0.4", "version": "3.1.0",
"description": "Modern URL shortener.", "description": "Modern URL shortener.",
"main": "./server/server.js", "main": "./server/server.js",
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development node --watch-path=./server server/server.js", "dev": "cross-env NODE_ENV=development node --watch-path=./server --watch-path=./custom server/server.js",
"start": "cross-env NODE_ENV=production node server/server.js", "start": "cross-env NODE_ENV=production node server/server.js",
"migrate": "knex migrate:latest", "migrate": "knex migrate:latest",
"migrate:make": "knex migrate:make", "migrate:make": "knex migrate:make",

View File

@ -10,11 +10,18 @@ const supportedDBClients = [
"mysql2" "mysql2"
]; ];
// make sure custom alphabet is not empty
if (process.env.LINK_CUSTOM_ALPHABET === "") {
delete process.env.LINK_CUSTOM_ALPHABET;
}
const env = cleanEnv(process.env, { const env = cleanEnv(process.env, {
PORT: num({ default: 3000 }), PORT: num({ default: 3000 }),
SITE_NAME: str({ example: "Kutt", default: "Kutt" }), SITE_NAME: str({ example: "Kutt", default: "Kutt" }),
DEFAULT_DOMAIN: str({ example: "kutt.it", default: "localhost:3000" }), DEFAULT_DOMAIN: str({ example: "kutt.it", default: "localhost:3000" }),
LINK_LENGTH: num({ default: 6 }), LINK_LENGTH: num({ default: 6 }),
LINK_CUSTOM_ALPHABET: str({ default: "abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789" }),
TRUST_PROXY: bool({ default: true }),
DB_CLIENT: str({ choices: supportedDBClients, default: "sqlite3" }), DB_CLIENT: str({ choices: supportedDBClients, default: "sqlite3" }),
DB_FILENAME: str({ default: "db/data" }), DB_FILENAME: str({ default: "db/data" }),
DB_HOST: str({ default: "localhost" }), DB_HOST: str({ default: "localhost" }),

View File

@ -338,7 +338,7 @@ async function editAdmin(req, res) {
res.render("partials/admin/links/edit", { res.render("partials/admin/links/edit", {
swap_oob: true, swap_oob: true,
success: "Link has been updated.", success: "Link has been updated.",
...utils.sanitize.linkAdmin({ ...updatedLink }), ...utils.sanitize.link_admin({ ...updatedLink }),
}); });
return; return;
} }

View File

@ -29,6 +29,7 @@ function config(req, res, next) {
res.locals.disallow_registration = env.DISALLOW_REGISTRATION; res.locals.disallow_registration = env.DISALLOW_REGISTRATION;
res.locals.mail_enabled = env.MAIL_ENABLED; res.locals.mail_enabled = env.MAIL_ENABLED;
res.locals.report_email = env.REPORT_EMAIL; res.locals.report_email = env.REPORT_EMAIL;
res.locals.custom_styles = utils.getCustomCSSFileNames();
next(); next();
} }

View File

@ -45,7 +45,7 @@ const createLink = [
.trim() .trim()
.isLength({ min: 1, max: 64 }) .isLength({ min: 1, max: 64 })
.withMessage("Custom URL length must be between 1 and 64.") .withMessage("Custom URL length must be between 1 and 64.")
.custom(value => /^[a-zA-Z0-9-_]+$/g.test(value)) .custom(value => utils.customAddressRegex.test(value) || utils.customAlphabetRegex.test(value))
.withMessage("Custom URL is not valid.") .withMessage("Custom URL is not valid.")
.custom(value => !utils.preservedURLs.some(url => url.toLowerCase() === value)) .custom(value => !utils.preservedURLs.some(url => url.toLowerCase() === value))
.withMessage("You can't use this custom URL."), .withMessage("You can't use this custom URL."),
@ -120,7 +120,7 @@ const editLink = [
.trim() .trim()
.isLength({ min: 1, max: 64 }) .isLength({ min: 1, max: 64 })
.withMessage("Custom URL length must be between 1 and 64.") .withMessage("Custom URL length must be between 1 and 64.")
.custom(value => /^[a-zA-Z0-9-_]+$/g.test(value)) .custom(value => utils.customAddressRegex.test(value) || utils.customAlphabetRegex.test(value))
.withMessage("Custom URL is not valid") .withMessage("Custom URL is not valid")
.custom(value => !utils.preservedURLs.some(url => url.toLowerCase() === value)) .custom(value => !utils.preservedURLs.some(url => url.toLowerCase() === value))
.withMessage("You can't use this custom URL."), .withMessage("You can't use this custom URL."),

View File

@ -29,17 +29,20 @@ require("./passport");
// create express app // create express app
const app = express(); const app = express();
// this tells the express app that the app is running behind a proxy server // this tells the express app that it's running behind a proxy server
// and thus it should get the IP address from the proxy server // and thus it should get the IP address from the proxy server
// IMPORTANT: users might be able to override their IP address and this if (env.TRUST_PROXY) {
// might allow users to bypass the rate limit or lead to incorrect link stats app.set("trust proxy", true);
// read the Kutt documentation to learn how prevent users from changing their real IP address }
app.set("trust proxy", true);
app.use(helmet({ contentSecurityPolicy: false })); app.use(helmet({ contentSecurityPolicy: false }));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
// serve static
app.use("/images", express.static("custom/images"));
app.use("/css", express.static("custom/css", { extensions: ["css"] }));
app.use(express.static("static")); app.use(express.static("static"));
app.use(passport.initialize()); app.use(passport.initialize());
@ -47,8 +50,12 @@ app.use(locals.isHTML);
app.use(locals.config); app.use(locals.config);
// template engine / serve html // template engine / serve html
app.set("view engine", "hbs"); app.set("view engine", "hbs");
app.set("views", path.join(__dirname, "views")); app.set("views", [
path.join(__dirname, "../custom/views"),
path.join(__dirname, "views"),
]);
utils.registerHandlebarsHelpers(); utils.registerHandlebarsHelpers();
// if is custom domain, redirect to the set homepage // if is custom domain, redirect to the set homepage

View File

@ -1,7 +1,8 @@
const { differenceInDays, differenceInHours, differenceInMonths, differenceInMilliseconds, addDays, subHours, subDays, subMonths, subYears, format } = require("date-fns"); const { differenceInDays, differenceInHours, differenceInMonths, differenceInMilliseconds, addDays, subHours, subDays, subMonths, subYears, format } = require("date-fns");
const { customAlphabet } = require("nanoid"); const { customAlphabet } = require("nanoid");
const JWT = require("jsonwebtoken"); const JWT = require("jsonwebtoken");
const path = require("path"); const path = require("node:path");
const fs = require("node:fs");
const hbs = require("hbs"); const hbs = require("hbs");
const ms = require("ms"); const ms = require("ms");
@ -10,10 +11,7 @@ const knexUtils = require("./knex");
const knex = require("../knex"); const knex = require("../knex");
const env = require("../env"); const env = require("../env");
const nanoid = customAlphabet( const nanoid = customAlphabet(env.LINK_CUSTOM_ALPHABET, env.LINK_LENGTH);
"abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789",
env.LINK_LENGTH
);
class CustomError extends Error { class CustomError extends Error {
constructor(message, statusCode, data) { constructor(message, statusCode, data) {
@ -26,6 +24,12 @@ class CustomError extends Error {
const urlRegex = /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i; const urlRegex = /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
const charsNeedEscapeInRegExp = ".$*+?()[]{}|^-";
const customAlphabetEscaped = env.LINK_CUSTOM_ALPHABET
.split("").map(c => charsNeedEscapeInRegExp.includes(c) ? "\\" + c : c).join("");
const customAlphabetRegex = new RegExp(`^[${customAlphabetEscaped}_-]+$`);
const customAddressRegex = new RegExp("^[a-zA-Z0-9-_]+$");
function isAdmin(user) { function isAdmin(user) {
return user.role === ROLES.ADMIN; return user.role === ROLES.ADMIN;
} }
@ -360,14 +364,42 @@ function registerHandlebarsHelpers() {
return val; return val;
}); });
hbs.registerPartials(path.join(__dirname, "../views/partials"), function (err) {}); hbs.registerPartials(path.join(__dirname, "../views/partials"), function (err) {});
const customPartialsPath = path.join(__dirname, "../../custom/views/partials");
const customPartialsExist = fs.existsSync(customPartialsPath);
if (customPartialsExist) {
hbs.registerPartials(customPartialsPath, function (err) {});
}
}
// grab custom styles file name from the custom/css folder
const custom_css_file_names = [];
const customCSSPath = path.join(__dirname, "../../custom/css");
const customCSSExists = fs.existsSync(customCSSPath);
if (customCSSExists) {
fs.readdir(customCSSPath, function(error, files) {
if (error) {
console.warn("Could not read the custom CSS folder:", error);
} else {
files.forEach(function(file_name) {
custom_css_file_names.push(file_name);
});
}
})
}
function getCustomCSSFileNames() {
return custom_css_file_names;
} }
module.exports = { module.exports = {
addProtocol, addProtocol,
customAddressRegex,
customAlphabetRegex,
CustomError, CustomError,
dateToUTC, dateToUTC,
deleteCurrentToken, deleteCurrentToken,
generateId, generateId,
getCustomCSSFileNames,
getDifferenceFunction, getDifferenceFunction,
getInitStats, getInitStats,
getShortURL, getShortURL,

View File

@ -24,6 +24,9 @@
<meta name="description" content="{{site_name}} is a free and open source URL shortener with custom domains and stats." /> <meta name="description" content="{{site_name}} is a free and open source URL shortener with custom domains and stats." />
<title>{{site_name}} | {{title}}</title> <title>{{site_name}} | {{title}}</title>
<link rel="stylesheet" href="/css/styles.css"> <link rel="stylesheet" href="/css/styles.css">
{{#each custom_styles}}
<link rel="stylesheet" href="/css/{{this}}">
{{/each}}
{{{block "stylesheets"}}} {{{block "stylesheets"}}}
</head> </head>
<body> <body>

View File

@ -25,7 +25,7 @@
{{#if errors.target}}<p class="error">{{errors.target}}</p>{{/if}} {{#if errors.target}}<p class="error">{{errors.target}}</p>{{/if}}
</label> </label>
<label class="{{#if errors.address}}error{{/if}}"> <label class="{{#if errors.address}}error{{/if}}">
{{domain}}/ <span id="edit-link-domain-{{id}}" hx-preserve="true">{{domain}}/</span>
<input <input
id="edit-address-{{id}}" id="edit-address-{{id}}"
name="address" name="address"

View File

@ -1,7 +1,6 @@
<footer> <footer>
<p> <p>
Made with love by <a href="https://thedevs.network" title="The Devs" target="_blank" rel="noopener noreferrer">The Devs</a>. <span>|</span> Powered by <a href="https://github.com/thedevs-network/kutt" title="The Devs" target="_blank" rel="noopener noreferrer">Kutt</a> <span>|</span>
<a href="https://github.com/thedevs-network/kutt" title="GitHub" target="_blank" rel="noopener noreferrer">GitHub</a> <span>|</span>
<a href="/terms" title="Terms of Service">Terms of Service</a> <a href="/terms" title="Terms of Service">Terms of Service</a>
{{#if report_email}} {{#if report_email}}
<span>|</span> <span>|</span>

View File

@ -1,7 +1,7 @@
<header> <header>
<div class="logo-wrapper"> <div class="logo-wrapper">
<a class="logo nav" href="/" title="Homepage"> <a class="logo nav" href="/" title="Homepage">
<img src="/images/logo.svg" alt="kutt" width="18" height="24" /> <img src="/images/logo.png" alt="kutt" width="18" height="24" />
{{site_name}} {{site_name}}
</a> </a>
<ul class="logo-links"> <ul class="logo-links">

View File

@ -25,7 +25,7 @@
{{#if errors.target}}<p class="error">{{errors.target}}</p>{{/if}} {{#if errors.target}}<p class="error">{{errors.target}}</p>{{/if}}
</label> </label>
<label class="{{#if errors.address}}error{{/if}}"> <label class="{{#if errors.address}}error{{/if}}">
{{domain}}/ <span id="edit-link-domain-{{id}}" hx-preserve="true">{{domain}}/</span>
<input <input
id="edit-address-{{id}}" id="edit-address-{{id}}"
name="address" name="address"

View File

@ -8,26 +8,39 @@
:root { :root {
--bg-color: hsl(206, 12%, 95%); --bg-color: hsl(206, 12%, 95%);
--text-color: hsl(200, 35%, 25%); --text-color: hsl(200, 35%, 25%);
--color-primary: #2196f3; --color-primary: hsl(207, 90%, 54%);
--outline-color: #14e0ff; --outline-color: hsl(188, 100%, 54%);
--button-bg: linear-gradient(to right, #e0e0e0, #bdbdbd); --button-bg: linear-gradient(to right, #e0e0e0, #bdbdbd);
--button-bg-box-shadow-color: rgba(160, 160, 160, 0.5); --button-bg-box-shadow-color: rgba(160, 160, 160, 0.5);
--button-bg-primary: linear-gradient(to right, #42a5f5, #2979ff); --button-bg-primary: linear-gradient(to right, hsl(207, 90%, 61%), hsl(218, 100%, 58%));
--button-bg-primary-box-shadow-color: rgba(66, 165, 245, 0.5); --button-bg-primary-box-shadow-color: hsla(207, 90%, 61%, 0.5);
--button-bg-secondary: linear-gradient(to right, #7e57c2, #6200ea); --button-bg-secondary: linear-gradient(to right, hsl(262, 47%, 55%), hsl(265, 100%, 46%));
--button-bg-secondary-box-shadow-color: rgba(81, 45, 168, 0.5); --button-bg-secondary-box-shadow-color: hsla(258, 58%, 42%, 0.5);
--button-bg-danger: linear-gradient(to right, #ee3b3b, #e11c1c); --button-bg-danger: linear-gradient(to right, hsl(0, 84%, 58%), hsl(0, 78%, 50%));
--button-bg-danger-box-shadow-color: rgba(168, 45, 45, 0.5); --button-bg-danger-box-shadow-color: hsla(0, 58%, 42%, 0.5);
--button-bg-success: linear-gradient(to right, #31b647, #26be3f); --button-bg-success: linear-gradient(to right, hsl(130, 58%, 45%), hsl(130, 67%, 45%));
--button-bg-success-box-shadow-color: rgba(25, 221, 51, 50%); --button-bg-success-box-shadow-color: hsla(128, 80%, 48%, 0.5);
--features-bg: hsl(230, 15%, 92%); --button-action-shadow-color: hsla(200, 15%, 60%, 0.12);
--extensions-bg: hsl(230, 15%, 20%); --underline-color: hsl(200, 35%, 65%);
--send-icon-hover-color: #673ab7; --secondary-text-color: hsl(200, 14%, 60%);
--send-icon-hover-color: hsl(262, 52%, 47%);
--send-spinner-icon-color: hsl(200, 15%, 70%); --send-spinner-icon-color: hsl(200, 15%, 70%);
--success-icon-color: hsl(144, 40%, 57%); --success-icon-color: hsl(144, 40%, 57%);
--error-icon-color: #f24f4f; --error-icon-color: hsl(0, 86%, 63%);
--copy-icon-color: hsl(144, 40%, 57%); --copy-icon-color: hsl(144, 40%, 57%);
--copy-icon-bg-color: hsl(144, 100%, 96%); --copy-icon-bg-color: hsl(144, 100%, 96%);
--copy-icon-shadow-color: hsla(200, 15%, 60%, 0.12);
--focus-outline-color: hsla(207, 90%, 61%, 0.5);
--checkbox-bg-color: hsl(262, 47%, 63%);
--input-shadow-color: hsla(200, 15%, 70%, 0.2);
--input-hover-shadow-color: hsla(200, 15%, 70%, 0.4);
--input-label-color: hsl(200, 35%, 25%);
--table-bg-color: hsl(200, 12%, 95%);
--table-shadow-color: hsla(200, 20%, 70%, 0.3);
--table-tr-border-color: hsl(200, 14%, 94%);
--table-tr-hover-bg-color: hsl(200, 14%, 98%);
--table-head-tr-border-color: hsl(200, 14%, 90%);
--table-status-gray-bg-color: hsl(200, 12%, 95%);
--keyframe-slidey-offset: 0; --keyframe-slidey-offset: 0;
} }
@ -233,7 +246,7 @@ button.action {
padding: 5px; padding: 5px;
width: 24px; width: 24px;
height: 24px; height: 24px;
box-shadow: 0 2px 1px hsla(200, 15%, 60%, 0.12); box-shadow: 0 2px 1px var(--button-action-shadow-color);
} }
a.button.action:disabled, a.button.action:disabled,
@ -441,7 +454,7 @@ input[type="password"] {
border-radius: 100px; border-radius: 100px;
border-bottom: 5px solid #f5f5f5; border-bottom: 5px solid #f5f5f5;
border-bottom-width: 5px; border-bottom-width: 5px;
box-shadow: 0 10px 35px hsla(200, 15%, 70%, 0.2); box-shadow: 0 10px 35px var(--input-shadow-color);
transition: all 0.5s ease-out; transition: all 0.5s ease-out;
} }
@ -450,7 +463,7 @@ input[type="text"]:focus,
input[type="email"]:focus, input[type="email"]:focus,
input[type="password"]:focus { input[type="password"]:focus {
outline: none; outline: none;
box-shadow: 0 20px 35px hsla(200, 15%, 70%, 0.4); box-shadow: 0 20px 35px var(--input-hover-shadow-color);
} }
input[type="text"]::placeholder, input[type="text"]::placeholder,
@ -478,7 +491,7 @@ select {
letter-spacing: 0.05em; letter-spacing: 0.05em;
color: #444; color: #444;
background-color: white; background-color: white;
box-shadow: 0 10px 35px hsla(200, 15%, 70%, 0.2); box-shadow: 0 10px 35px var(--input-shadow-color);
border: none; border: none;
border-radius: 100px; border-radius: 100px;
border-bottom: 5px solid #f5f5f5; border-bottom: 5px solid #f5f5f5;
@ -493,7 +506,7 @@ select {
select:focus { select:focus {
outline: none; outline: none;
box-shadow: 0 20px 35px hsla(200, 15%, 70%, 0.4); box-shadow: 0 20px 35px var(--input-hover-shadow-color)
} }
.error select { .error select {
@ -518,7 +531,7 @@ input[type="checkbox"] {
} }
input[type="checkbox"]:focus { input[type="checkbox"]:focus {
outline: 3px solid rgba(65, 164, 245, 0.5); outline: 3px solid var(--focus-outline-color);
} }
input[type="checkbox"]::after { input[type="checkbox"]::after {
@ -530,7 +543,7 @@ input[type="checkbox"]::after {
height: 80%; height: 80%;
display: block; display: block;
border-radius: 2px; border-radius: 2px;
background-color: #9575cd; background-color: var(--checkbox-bg-color);
box-shadow: 0 2px 4px rgba(50, 50, 50, 0.2); box-shadow: 0 2px 4px rgba(50, 50, 50, 0.2);
cursor: pointer; cursor: pointer;
opacity: 0; opacity: 0;
@ -568,7 +581,7 @@ select:has(option[value=""]:checked) {
label { label {
display: flex; display: flex;
color: rgb(41, 71, 86); color: var(--input-label-color);
font-size: 1rem; font-size: 1rem;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@ -607,7 +620,7 @@ table {
flex-direction: column; flex-direction: column;
background-color: white; background-color: white;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 6px 15px hsla(200, 20%, 70%, 0.3); box-shadow: 0 6px 15px var(--table-shadow-color);
text-align: center; text-align: center;
overflow: auto; overflow: auto;
} }
@ -638,7 +651,7 @@ table tfoot {
table tr { table tr {
padding: 0 0.5rem; padding: 0 0.5rem;
border-bottom: 1px solid hsl(200, 14%, 94%); border-bottom: 1px solid var(--table-tr-border-color);
} }
table th, table th,
@ -665,18 +678,18 @@ table tbody + tfoot {
} }
table thead { table thead {
background-color: hsl(200, 12%, 95%); background-color: var(--table-bg-color);
border-top-right-radius: 12px; border-top-right-radius: 12px;
border-top-left-radius: 12px; border-top-left-radius: 12px;
font-weight: bold; font-weight: bold;
} }
table thead tr { table thead tr {
border-bottom: 1px solid hsl(200, 14%, 90%); border-bottom: 1px solid var(--table-head-tr-border-color);
} }
table tfoot { table tfoot {
background-color: hsl(200, 12%, 95%); background-color: var(--table-bg-color);
border-bottom-right-radius: 12px; border-bottom-right-radius: 12px;
border-bottom-left-radius: 12px; border-bottom-left-radius: 12px;
} }
@ -1159,7 +1172,7 @@ main #shorturl h1 {
main #shorturl h1.link { main #shorturl h1.link {
cursor: pointer; cursor: pointer;
border-bottom-color: hsl(200, 35%, 65%); border-bottom-color: var(--underline-color);
transition: opacity 0.3s ease-in-out; transition: opacity 0.3s ease-in-out;
--keyframe-slidey-offset: -10px; --keyframe-slidey-offset: -10px;
animation: fadein 0.2s ease-in-out, slidey 0.2s ease-in-out; animation: fadein 0.2s ease-in-out, slidey 0.2s ease-in-out;
@ -1189,7 +1202,7 @@ main #shorturl h1.link:hover {
border-radius: 100%; border-radius: 100%;
background-color: var(--copy-icon-bg-color); background-color: var(--copy-icon-bg-color);
transition: transform 0.4s ease-out; transition: transform 0.4s ease-out;
box-shadow: 0 2px 1px hsla(200, 15%, 60%, 0.12); box-shadow: 0 2px 1px var(--copy-icon-shadow-color);
cursor: pointer; cursor: pointer;
--keyframe-slidey-offset: -10px; --keyframe-slidey-offset: -10px;
animation: slidey 0.2s ease-in-out; animation: slidey 0.2s ease-in-out;
@ -1204,7 +1217,7 @@ main #shorturl h1.link:hover {
} }
.clipboard button:focus { .clipboard button:focus {
outline: 3px solid rgba(65, 164, 245, 0.5); outline: 3px solid var(--focus-outline-color);
} }
.clipboard svg { .clipboard svg {
@ -1469,7 +1482,7 @@ table .short-link-wrapper { display: flex; align-items: center; }
margin-top: 0; margin-top: 0;
} }
#main-table-wrapper table .status.gray { background-color: hsl(200, 12%, 95%); } #main-table-wrapper table .status.gray { background-color: var(--table-status-gray-bg-color); }
#main-table-wrapper table .status.green { background-color: hsl(102.4, 100%, 93.3%); } #main-table-wrapper table .status.green { background-color: hsl(102.4, 100%, 93.3%); }
#main-table-wrapper table .status.red { background-color: hsl(0, 100%, 96.7%); } #main-table-wrapper table .status.red { background-color: hsl(0, 100%, 96.7%); }
@ -1547,7 +1560,7 @@ table .short-link-wrapper { display: flex; align-items: center; }
} }
#main-table-wrapper table tbody tr:hover { #main-table-wrapper table tbody tr:hover {
background-color: hsl(200, 14%, 98%); background-color: var(--table-tr-hover-bg-color);
} }
#main-table-wrapper table tbody td.right-fade:after { #main-table-wrapper table tbody td.right-fade:after {
@ -1561,10 +1574,9 @@ table .short-link-wrapper { display: flex; align-items: center; }
} }
#main-table-wrapper table tbody tr:hover td.right-fade:after { #main-table-wrapper table tbody tr:hover td.right-fade:after {
background: linear-gradient(to left, hsl(200, 14%, 98%), rgba(255, 255, 255, 0.001)); background: linear-gradient(to left, var(--table-tr-hover-bg-color), rgba(255, 255, 255, 0.001));
} }
#main-table-wrapper table .clipboard { margin-right: 0.5rem; } #main-table-wrapper table .clipboard { margin-right: 0.5rem; }
#main-table-wrapper table .clipboard svg.check { width: 24px; } #main-table-wrapper table .clipboard svg.check { width: 24px; }
@ -1858,7 +1870,7 @@ form#delete-account.htmx-request .spinner { display: block; }
align-items: stretch; align-items: stretch;
background-color: white; background-color: white;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 6px 15px hsla(200, 20%, 70%, 0.3); box-shadow: 0 6px 15px var(--table-shadow-color);
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
} }
@ -1867,7 +1879,7 @@ form#delete-account.htmx-request .spinner { display: block; }
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
background-color: hsl(200, 12%, 95%); background-color: var(--table-bg-color);
justify-content: space-between; justify-content: space-between;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
} }
@ -1894,12 +1906,12 @@ form#delete-account.htmx-request .spinner { display: block; }
.stats-period span.total-in-period { .stats-period span.total-in-period {
font-weight: bold; font-weight: bold;
border-bottom: 1px dotted hsl(200, 35%, 65%); border-bottom: 1px dotted var(--underline-color);
} }
p.last-update { p.last-update {
font-size: 14px; font-size: 14px;
color: hsl(200, 14%, 60%); color: var(--secondary-text-color);
margin: 0.75rem 0 0; margin: 0.75rem 0 0;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 108 KiB

BIN
static/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

View File

@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 25 33" width="25" height="33"><defs><clipPath id="_clipPath_5Zq5Srm6iOgXYvO1ZpAW2QRumxRsERtf"><rect width="25" height="33"/></clipPath></defs><g clip-path="url(#_clipPath_5Zq5Srm6iOgXYvO1ZpAW2QRumxRsERtf)"><rect width="25" height="33" style="fill:rgb(243,243,243)"/><image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADcAAAA0CAYAAAAuT6DoAAAEvUlEQVRoQ+2ZTY/TRhjH/+OXZDfZpWSzTugrByRU1JUolANQhIRaqWr3VPW0alWJGwdQT9BPARz4FKgX1C9RVbCot6qnoiIhBLRSgV22G3umehyPM3aceMYv2QUlUuR1bCfzm98zz/PYy/AGv9gbzIY53Otqd25ubm4fzkBVYan7PWKWc6A7qKwxlblWfl+tsNoDvP77tniwBYhAwOIAOMBoGwCMhkj7tKX96Fh4XJ6XPj8ArOi6lmvhzFF+5/NT7a+rNKsNd+X+tiCwBJAKFoEkwDOAaAJYxnXH3uG/fP/F8tk9gbu8uSXIAr0zzRF4njnBRqYVc3Tdh2+HcJ8CqCxUdc2xy5tbfCwUwxAUidDTNkfNkRKy6yf57XPHlzcUc6UhjeFic+kQTa05RMeZYMNQnrTm6LgAvjoxBkecpQB14MJzEuZ8JQSjpEAwYyGbBQSWuebWPxFpc6Uzah5cfFzCEYQaekKC6mZL5Tyb1iAHmg7DD+u71zudztUJCaWQQWM4RoaibJcAC0MvVSaysmXKnMUZjvQEvjnjE9y1KaFoDGgGp4TeWFngGtkyURdZXOc+WxM4dSSQcNPC0QhwGlziWBiWShIhS6MCHtU/MhVNQFYSYXyYPBJ1TjBcvMCxuuTf6Ha7alhmgdQEd+9lWArCrKcC0JqjuY7CUrdDIVDqUN7rAN+d5wiCgODSYVkKUNvcrbuPuG0BPhwIxmADsBiwPbDwyrfxYgfY2WWjZKNOQNyRJM1Rmdg4y3F4VYBzLuHSJaAwoBkcAxw2hLKjt2MBFgDaCmFhx7fx+LmNJy8sBIOM3jLMlsNO5fAK8O05Ug74vn/D8zwyl7Xm0oBa4WkGZyE0pgLRPhkNtwSugP7zysGfzxz8+1xpqiHXHMPG6aE1xpiE+1HJlipAIXtmcDnm0oAS9O+XLjYfOPB3o5ZLAF+ucXz8wUgTrTnP81S4vPDMtTcJLv05u3X3USAN6ZiLjUYTwrmF3/5q4uFTYP0jgePvD8dG1ug1GAxu9vt9GZZy4NPsVQxX0Jwauv8NGljrjaAk4AS4UvZmZo6SkExG3UUXB5sUtMMX2YvgstZcYXtmcBWYkyHdW2qgRbR7ABf+ptrEyjqnmy3Ta041JxPPoQNNuNGv+L5Pa24m5rLhKjRHZaPdcLCyaOeFpbrujOqdWSkwqHM65igqveUmXGvqmpsRXMXmaAIWXBvdtjvThDKTNSezZ295AYGfqHOTbNUYljWYo8ag027CAZcdyrTaVhlcwl4d2VKaazUctF1m2n6lJ0FN7sMSM/ZJ8oP4eAhXkzn63pWWm74ryOtOSsPFE1CnOTL41mLjp35vddJzy8rvCuLuiP6o29zBJJzO2ircOKejle4KwjvxKjsUueZoe6DV+PmQt5r1j5BC1nTWXGyvbnNLC+6v7/a99D9CJtnJtWYCR2Ep6jTXTsJNG7wWmBHcnT+ePt7xxQLdqMhnKLIZppSqPmqgfdn9y0cPchteG51P3xV+bgFLTee21125lJO9tcGM4JQfzSsfOeMrfNgIrChcIosWHqr+hcZQVQ6wLpOFoaqEUx2UBS0NVOVg9INrD84sO9N7MGT9n5zD6c/V/jpzbm5/+dAfzf8lAKBTQdWwRQAAAABJRU5ErkJggg==" x="-9.861" y="-8.951" width="54.552" height="51.824" style=""/></g></svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB