mirror of
https://github.com/glenndehaan/unifi-voucher-site.git
synced 2026-04-05 00:44:18 -04:00
@@ -5,7 +5,7 @@
|
||||
#
|
||||
# Define OS
|
||||
#
|
||||
FROM alpine:3.20 AS dependencies
|
||||
FROM alpine:3.21 AS dependencies
|
||||
|
||||
#
|
||||
# Basic OS management
|
||||
@@ -34,7 +34,7 @@ RUN npm ci --only=production && npm cache clean --force
|
||||
#
|
||||
# Define OS
|
||||
#
|
||||
FROM alpine:3.20 AS css
|
||||
FROM alpine:3.21 AS css
|
||||
|
||||
#
|
||||
# Basic OS management
|
||||
@@ -69,7 +69,7 @@ RUN npm run build
|
||||
#
|
||||
# Define OS
|
||||
#
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
#
|
||||
# Basic OS management
|
||||
|
||||
290
README.md
290
README.md
@@ -6,7 +6,7 @@ UniFi Voucher Site is a web-based platform for generating and managing UniFi net
|
||||
|
||||

|
||||
|
||||
> Upgrading from 3.x to 4.x? Please take a look at the [migration guide](#migration-from-3x-to-4x)
|
||||
> Upgrading from 4.x to 5.x? Please take a look at the [migration guide](#migration-from-4x-to-5x)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -152,7 +152,7 @@ To install the UniFi Voucher Site add-on for Home Assistant, follow these steps:
|
||||
|
||||
## Development
|
||||
|
||||
- Install Node.js 20.0 or higher.
|
||||
- Install Node.js 22.0 or higher.
|
||||
- Run `npm ci` in the root folder
|
||||
- Run `npm start` & `npm run tailwind` in the root folder
|
||||
|
||||
@@ -181,20 +181,36 @@ the different endpoints available in the API:
|
||||
- Access: Open
|
||||
- Response Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"endpoints": [
|
||||
"/api",
|
||||
"/api/types",
|
||||
"/api/voucher/:type",
|
||||
"/api/vouchers"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"endpoints": [
|
||||
{
|
||||
"method": "GET",
|
||||
"endpoint": "/api"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"endpoint": "/api/types"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"endpoint": "/api/languages"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"endpoint": "/api/vouchers"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"endpoint": "/api/voucher"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **`/api/types`**
|
||||
- Method: GET
|
||||
@@ -203,44 +219,55 @@ the different endpoints available in the API:
|
||||
- Access: Open
|
||||
- Response Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"types": [
|
||||
{
|
||||
"expiration": "480",
|
||||
"usage": "0",
|
||||
"raw": "480,0,,,"
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"types": [
|
||||
{
|
||||
"expiration": "480",
|
||||
"usage": "0",
|
||||
"raw": "480,0,,,"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
3. **`/api/voucher/:type`**
|
||||
3. **`/api/languages`**
|
||||
- Method: GET
|
||||
- Description: Generates a voucher of the specified type.
|
||||
- Parameters:
|
||||
- `type` (string): The type of voucher to generate.
|
||||
- Description: Retrieves a list of available languages supported by the system.
|
||||
- Response Format: JSON
|
||||
- Access: Protected by Bearer Token
|
||||
- Access: Open
|
||||
- Response Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"voucher": "12345-67890"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> This endpoint is protected by a security mechanism. To access it, users need to include a bearer token in the
|
||||
request authorization header. The token must match the value of the `AUTH_INTERNAL_BEARER_TOKEN` environment variable. Without
|
||||
this token, access to the endpoint will be denied.
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"languages": [
|
||||
{
|
||||
"code": "en",
|
||||
"name": "English"
|
||||
},
|
||||
{
|
||||
"code": "de",
|
||||
"name": "German"
|
||||
},
|
||||
{
|
||||
"code": "nl",
|
||||
"name": "Dutch"
|
||||
},
|
||||
{
|
||||
"code": "pl",
|
||||
"name": "Polish"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **`/api/vouchers`**
|
||||
- Method: GET
|
||||
@@ -249,37 +276,88 @@ the different endpoints available in the API:
|
||||
- Access: Protected by Bearer Token
|
||||
- Response Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"vouchers": [
|
||||
{
|
||||
"code": "15695-53133",
|
||||
"type": "multi",
|
||||
"duration": 60,
|
||||
"data_limit": "200",
|
||||
"download_limit": "5000",
|
||||
"upload_limit": "2000"
|
||||
},
|
||||
{
|
||||
"code": "03004-59449",
|
||||
"type": "single",
|
||||
"duration": 480,
|
||||
"data_limit": null,
|
||||
"download_limit": null,
|
||||
"upload_limit": null
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"vouchers": [
|
||||
{
|
||||
"id": "67bded6766f89f2a7ba6731f",
|
||||
"code": "15695-53133",
|
||||
"type": "multi",
|
||||
"duration": 60,
|
||||
"data_limit": "200",
|
||||
"download_limit": "5000",
|
||||
"upload_limit": "2000"
|
||||
},
|
||||
{
|
||||
"id": "67bdecd166f89f2a7ba67317",
|
||||
"code": "03004-59449",
|
||||
"type": "single",
|
||||
"duration": 480,
|
||||
"data_limit": null,
|
||||
"download_limit": null,
|
||||
"upload_limit": null
|
||||
}
|
||||
],
|
||||
"updated": 1712934667937
|
||||
}
|
||||
],
|
||||
"updated": 1712934667937
|
||||
}
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
> This endpoint is protected by a security mechanism. To access it, users need to include a bearer token in the
|
||||
request authorization header. The token must match the value of the `AUTH_INTERNAL_BEARER_TOKEN` environment variable. Without
|
||||
this token, access to the endpoint will be denied.
|
||||
> This endpoint is protected by a security mechanism. To access it, users need to include a bearer token in the
|
||||
request authorization header. The token must match the value of the `AUTH_INTERNAL_BEARER_TOKEN` environment variable. Without
|
||||
this token, access to the endpoint will be denied.
|
||||
|
||||
5. **`/api/voucher`**
|
||||
- Method: POST
|
||||
- Description: Generates a voucher of the specified type. Optionally sends an email.
|
||||
- Response Format: JSON
|
||||
- Access: Protected by Bearer Token
|
||||
- Body:
|
||||
- Generate Voucher:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "480,0,,,"
|
||||
}
|
||||
```
|
||||
|
||||
- Generate Voucher and Send Email *(**Warning**: Email module needs to be setup!)*:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "480,0,,,",
|
||||
"email": {
|
||||
"language": "en",
|
||||
"address": "user@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Response Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"voucher": {
|
||||
"id": "67bdf77b66f89f2a7ba678f7",
|
||||
"code": "02791-97992"
|
||||
},
|
||||
"email": {
|
||||
"status": "SENT",
|
||||
"address": "user@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> This endpoint is protected by a security mechanism. To access it, users need to include a bearer token in the
|
||||
request authorization header. The token must match the value of the `AUTH_INTERNAL_BEARER_TOKEN` environment variable. Without
|
||||
this token, access to the endpoint will be denied.
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -518,6 +596,64 @@ Detailed information on the changes in each release can be found on the [GitHub
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Migration from 4.x to 5.x
|
||||
|
||||
When upgrading from 4.x to 5.x, the following changes need to be made:
|
||||
|
||||
1. **Updated `/api` Endpoint Response**
|
||||
- The `/api` endpoint now returns a structured response that includes the HTTP method for each available endpoint. Update API integrations to handle the new response structure.
|
||||
|
||||
**Previous Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"endpoints": [
|
||||
"/api",
|
||||
"/api/types",
|
||||
"/api/voucher/:type",
|
||||
"/api/vouchers"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**New Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": null,
|
||||
"data": {
|
||||
"message": "OK",
|
||||
"endpoints": [
|
||||
{ "method": "GET", "endpoint": "/api" },
|
||||
{ "method": "GET", "endpoint": "/api/types" },
|
||||
{ "method": "GET", "endpoint": "/api/languages" },
|
||||
{ "method": "GET", "endpoint": "/api/vouchers" },
|
||||
{ "method": "POST", "endpoint": "/api/voucher" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **`/api/voucher/:type` Endpoint Replacement**
|
||||
- The **`/api/voucher/:type`** endpoint has been replaced by **`/api/voucher`**.
|
||||
- The method has changed from **`GET`** to **`POST`**.
|
||||
- The `:type` URL parameter is now passed in the **JSON body**.
|
||||
|
||||
**Example Request:**
|
||||
|
||||
```http
|
||||
POST /api/voucher
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"type": "480,0,,,"
|
||||
}
|
||||
```
|
||||
|
||||
### Migration from 3.x to 4.x
|
||||
|
||||
When upgrading from 3.x to 4.x, the following changes need to be made:
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/forms';
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
/*
|
||||
The default cursor has changed to `default` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an cursor
|
||||
utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
|
||||
2177
package-lock.json
generated
2177
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -5,18 +5,19 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "LOG_LEVEL=trace node server.js",
|
||||
"dev": "nodemon --watch . --exec 'LOG_LEVEL=trace node server.js'",
|
||||
"dev": "LOG_LEVEL=trace node --watch server.js",
|
||||
"tailwind": "tailwindcss -i ./css/style.css -o ./public/dist/style.css --watch",
|
||||
"build": "tailwindcss -i ./css/style.css -o ./public/dist/style.css --minify"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"author": "Glenn de Haan",
|
||||
"license": "MIT",
|
||||
"overrides": {
|
||||
"node-unifi@^2.5.1": {
|
||||
"axios": "1.7.4"
|
||||
"axios": "1.7.4",
|
||||
"tough-cookie": "5.1.1"
|
||||
},
|
||||
"express-openid-connect@^2.17.1": {
|
||||
"cookie": "0.7.0"
|
||||
@@ -31,15 +32,15 @@
|
||||
"js-logger": "^1.6.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-thermal-printer": "^4.4.3",
|
||||
"node-thermal-printer": "^4.4.4",
|
||||
"node-unifi": "^2.5.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"nodemailer": "^6.10.0",
|
||||
"pdfkit": "^0.16.0",
|
||||
"qrcode": "^1.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.0.9",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"nodemon": "^3.1.9",
|
||||
"tailwindcss": "^3.4.17"
|
||||
"tailwindcss": "^4.0.9"
|
||||
}
|
||||
}
|
||||
|
||||
184
server.js
184
server.js
@@ -106,6 +106,11 @@ app.use(locale({
|
||||
"default": "en-GB"
|
||||
}));
|
||||
|
||||
/**
|
||||
* Enable JSON
|
||||
*/
|
||||
app.use(express.json());
|
||||
|
||||
/**
|
||||
* Enable multer
|
||||
*/
|
||||
@@ -591,10 +596,26 @@ if(variables.serviceApi) {
|
||||
data: {
|
||||
message: 'OK',
|
||||
endpoints: [
|
||||
'/api',
|
||||
'/api/types',
|
||||
'/api/voucher/:type',
|
||||
'/api/vouchers'
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api/types'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api/languages'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
endpoint: '/api/vouchers'
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
endpoint: '/api/voucher'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
@@ -608,36 +629,19 @@ if(variables.serviceApi) {
|
||||
}
|
||||
});
|
||||
});
|
||||
app.get('/api/voucher/:type', [authorization.api], async (req, res) => {
|
||||
const typeCheck = (variables.voucherTypes).split(';').includes(req.params.type);
|
||||
|
||||
if(!typeCheck) {
|
||||
res.json({
|
||||
error: 'Unknown Type!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create voucher code
|
||||
const voucherCode = await unifi.create(types(req.params.type, true)).catch((e) => {
|
||||
res.json({
|
||||
error: e,
|
||||
data: {}
|
||||
});
|
||||
app.get('/api/languages', (req, res) => {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
languages: Object.keys(languages).map(language => {
|
||||
return {
|
||||
code: language,
|
||||
name: languages[language]
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
await updateCache();
|
||||
|
||||
if(voucherCode) {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
voucher: voucherCode
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
app.get('/api/vouchers', [authorization.api], async (req, res) => {
|
||||
res.json({
|
||||
@@ -646,6 +650,7 @@ if(variables.serviceApi) {
|
||||
message: 'OK',
|
||||
vouchers: cache.vouchers.map((voucher) => {
|
||||
return {
|
||||
id: voucher._id,
|
||||
code: `${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`,
|
||||
type: voucher.quota === 1 ? 'single' : voucher.quota === 0 ? 'multi' : 'multi',
|
||||
duration: voucher.duration,
|
||||
@@ -658,6 +663,119 @@ if(variables.serviceApi) {
|
||||
}
|
||||
});
|
||||
});
|
||||
app.post('/api/voucher', [authorization.api], async (req, res) => {
|
||||
// Verify valid body is sent
|
||||
if(!req.body || !req.body.type) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid Body!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if email body is set
|
||||
if(req.body.email) {
|
||||
// Check if email module is enabled
|
||||
if(variables.smtpFrom === '' || variables.smtpHost === '' || variables.smtpPort === '') {
|
||||
res.status(400).json({
|
||||
error: 'Email Not Configured!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if email body is correct
|
||||
if(!req.body.email.language || !req.body.email.address) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid Body!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if language is available
|
||||
if(!Object.keys(languages).includes(req.body.email.language)) {
|
||||
res.status(400).json({
|
||||
error: 'Unknown Language!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if type is implemented and valid
|
||||
const typeCheck = (variables.voucherTypes).split(';').includes(req.body.type);
|
||||
if(!typeCheck) {
|
||||
res.status(400).json({
|
||||
error: 'Unknown Type!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create voucher code
|
||||
const voucherCode = await unifi.create(types(req.body.type, true)).catch((e) => {
|
||||
res.status(500).json({
|
||||
error: e,
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
|
||||
// Update application cache
|
||||
await updateCache();
|
||||
|
||||
if(voucherCode) {
|
||||
// Locate voucher data within cache
|
||||
const voucherData = cache.vouchers.find(voucher => voucher.code === voucherCode.replaceAll('-', ''));
|
||||
if(!voucherData) {
|
||||
res.status(500).json({
|
||||
error: 'Invalid application cache!',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we should send and email
|
||||
if(req.body.email) {
|
||||
// Send mail
|
||||
const emailResult = await mail.send(req.body.email.address, voucherData, req.body.email.language).catch((e) => {
|
||||
res.status(500).json({
|
||||
error: e,
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
|
||||
// Verify is the email was sent successfully
|
||||
if(emailResult) {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
voucher: {
|
||||
id: voucherData._id,
|
||||
code: voucherCode
|
||||
},
|
||||
email: {
|
||||
status: 'SENT',
|
||||
address: req.body.email.address
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.json({
|
||||
error: null,
|
||||
data: {
|
||||
message: 'OK',
|
||||
voucher: {
|
||||
id: voucherData._id,
|
||||
code: voucherCode
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
content: ["./template/**/*.{html,js,ejs}"],
|
||||
darkMode: 'media',
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
variants: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms')
|
||||
]
|
||||
};
|
||||
/*
|
||||
This file is present to satisfy a requirement of the Tailwind CSS IntelliSense
|
||||
The rest of this file is intentionally empty.
|
||||
*/
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<h1 class="mt-5 text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-5xl">Page not found</h1>
|
||||
<p class="mt-4 text-base leading-7 text-gray-600 dark:text-gray-400">Sorry, we couldn’t find the page you’re looking for.</p>
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a href="<%= baseUrl %>/" class="rounded-md bg-sky-700 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Go back home</a>
|
||||
<a href="<%= baseUrl %>/" class="rounded-md bg-sky-700 px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Go back home</a>
|
||||
</div>
|
||||
<p class="mt-10 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<a href="https://github.com/glenndehaan/unifi-voucher-site" aria-label="GitHub Project Link" target="_blank" rel="noreferrer noopener" class="hover:text-gray-600 dark:hover:text-gray-500">
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
<img class="mx-auto h-24 w-auto" width="96" height="96" alt="UniFi Voucher Site Logo" src="<%= baseUrl %>/images/logo.png">
|
||||
<h1 class="mt-5 text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-5xl">Error</h1>
|
||||
<p class="mt-4 text-base leading-7 text-gray-600 dark:text-gray-400">Sorry, an unexpected error occurred.</p>
|
||||
<pre class="max-w-72 sm:max-w-lg mt-4 text-left text-xs text-gray-600 dark:text-gray-400 overflow-x-auto rounded-md border bg-white dark:bg-white/5 border-gray-300 dark:border-white/10 px-3 py-2 placeholder-gray-400 shadow-sm">
|
||||
<pre class="max-w-72 sm:max-w-lg mt-4 text-left text-xs text-gray-600 dark:text-gray-400 overflow-x-auto rounded-md border bg-white dark:bg-white/5 border-gray-300 dark:border-white/10 px-3 py-2 placeholder-gray-400 shadow-xs">
|
||||
<%= error %>
|
||||
</pre>
|
||||
<div class="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a href="<%= baseUrl %>/" class="rounded-md bg-sky-700 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Go back home</a>
|
||||
<a href="<%= baseUrl %>/" class="rounded-md bg-sky-700 px-3.5 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Go back home</a>
|
||||
</div>
|
||||
<p class="mt-10 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<a href="https://github.com/glenndehaan/unifi-voucher-site" aria-label="GitHub Project Link" target="_blank" rel="noreferrer noopener" class="hover:text-gray-600 dark:hover:text-gray-500">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="relative z-40" role="dialog" aria-modal="true">
|
||||
<div id="overlay" class="fixed inset-0 bg-gray-500 bg-opacity-75"></div>
|
||||
<div id="overlay" class="fixed inset-0 bg-gray-500/75"></div>
|
||||
|
||||
<div class="fixed overflow-hidden">
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
@@ -21,7 +21,7 @@
|
||||
<div>
|
||||
<label for="language" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Language</label>
|
||||
<div class="mt-2">
|
||||
<select id="language" name="language" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 [&_*]:text-black">
|
||||
<select id="language" name="language" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
|
||||
<% Object.keys(languages).forEach((language) => { %>
|
||||
<option value="<%= language %>"<%= language === defaultLanguage ? ' selected' : '' %>><%= languages[language] %> (<%= language %>)</option>
|
||||
<% }); %>
|
||||
@@ -33,7 +33,7 @@
|
||||
<ul role="list" class="max-h-96 h-96 overflow-y-auto mt-2 rounded-md border-0 divide-y divide-black/5 dark:divide-white/5 dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10">
|
||||
<% vouchers.forEach((voucher) => { %>
|
||||
<li class="relative flex items-center space-x-4 px-4 py-4">
|
||||
<input id="voucher-<%= voucher._id %>" aria-describedby="voucher-<%= voucher._id %>" name="vouchers" type="checkbox" value="<%= voucher._id %>" class="col-start-1 row-start-1 appearance-none rounded border-0 dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 focus:ring-offset-0">
|
||||
<input id="voucher-<%= voucher._id %>" aria-describedby="voucher-<%= voucher._id %>" name="vouchers" type="checkbox" value="<%= voucher._id %>" class="col-start-1 row-start-1 appearance-none rounded-sm border-0 dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 focus:ring-offset-0">
|
||||
<label for="voucher-<%= voucher._id %>" class="voucher min-w-0 flex-auto">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<h2 class="min-w-0 text-sm font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
@@ -96,9 +96,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-shrink-0 justify-end px-4 py-4">
|
||||
<button id="close" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" class="ml-4 inline-flex justify-center rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Print</button>
|
||||
<div class="flex shrink-0 justify-end px-4 py-4">
|
||||
<button id="close" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" class="ml-4 inline-flex justify-center rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Print</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="relative z-40" role="dialog" aria-modal="true">
|
||||
<div id="overlay" class="fixed inset-0 bg-gray-500 bg-opacity-75"></div>
|
||||
<div id="overlay" class="fixed inset-0 bg-gray-500/75"></div>
|
||||
|
||||
<div class="fixed overflow-hidden">
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
@@ -97,7 +97,7 @@
|
||||
<% } else { %>
|
||||
<% guests.forEach((guest) => { %>
|
||||
<div class="lg:col-start-3 lg:row-end-1">
|
||||
<div class="rounded-lg bg-gray-50 dark:bg-gray-800 shadow-sm ring-1 ring-gray-900/5 dark:ring-gray-50/25">
|
||||
<div class="rounded-lg bg-gray-50 dark:bg-gray-800 shadow-xs ring-1 ring-gray-900/5 dark:ring-gray-50/25">
|
||||
<dl class="flex flex-wrap">
|
||||
<div class="flex-auto pl-6 pt-6">
|
||||
<dt class="text-sm font-semibold leading-6 text-gray-900 dark:text-white"><%= guest.mac %></dt>
|
||||
@@ -158,8 +158,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-shrink-0 justify-end px-4 py-4">
|
||||
<button id="close" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Close</button>
|
||||
<div class="flex shrink-0 justify-end px-4 py-4">
|
||||
<button id="close" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="relative z-40" role="dialog" aria-modal="true">
|
||||
<div id="overlay" class="fixed inset-0 bg-gray-500 bg-opacity-75"></div>
|
||||
<div id="overlay" class="fixed inset-0 bg-gray-500/75"></div>
|
||||
|
||||
<div class="fixed overflow-hidden">
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
@@ -27,7 +27,7 @@
|
||||
<div>
|
||||
<label for="language" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Language</label>
|
||||
<div class="mt-2">
|
||||
<select id="language" name="language" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 [&_*]:text-black">
|
||||
<select id="language" name="language" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
|
||||
<% Object.keys(languages).forEach((language) => { %>
|
||||
<option value="<%= language %>"<%= language === defaultLanguage ? ' selected' : '' %>><%= languages[language] %> (<%= language %>)</option>
|
||||
<% }); %>
|
||||
@@ -38,9 +38,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-shrink-0 justify-end px-4 py-4">
|
||||
<button id="close" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" class="ml-4 inline-flex justify-center rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Send</button>
|
||||
<div class="flex shrink-0 justify-end px-4 py-4">
|
||||
<button id="close" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" class="ml-4 inline-flex justify-center rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="relative z-40" role="dialog" aria-modal="true">
|
||||
<div id="overlay" class="fixed inset-0 bg-gray-500 bg-opacity-75"></div>
|
||||
<div id="overlay" class="fixed inset-0 bg-gray-500/75"></div>
|
||||
|
||||
<div class="fixed overflow-hidden">
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
@@ -21,7 +21,7 @@
|
||||
<div>
|
||||
<label for="language" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Language</label>
|
||||
<div class="mt-2">
|
||||
<select id="language" name="language" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 [&_*]:text-black">
|
||||
<select id="language" name="language" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
|
||||
<% Object.keys(languages).forEach((language) => { %>
|
||||
<option value="<%= language %>"<%= language === defaultLanguage ? ' selected' : '' %>><%= languages[language] %> (<%= language %>)</option>
|
||||
<% }); %>
|
||||
@@ -32,9 +32,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-shrink-0 justify-end px-4 py-4">
|
||||
<button id="close" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" class="ml-4 inline-flex justify-center rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Print</button>
|
||||
<div class="flex shrink-0 justify-end px-4 py-4">
|
||||
<button id="close" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" class="ml-4 inline-flex justify-center rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Print</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<% if(error) { %>
|
||||
<div class="mt-5 rounded-md bg-red-700 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<svg class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
@@ -55,12 +55,12 @@
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Password</label>
|
||||
<div class="mt-2">
|
||||
<input type="password" id="password" name="password" required class="block w-full rounded-md border-0 dark:bg-white/5 py-1.5 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-inset focus:ring-sky-700 sm:text-sm sm:leading-6">
|
||||
<input type="password" id="password" name="password" required class="block w-full rounded-md border-0 dark:bg-white/5 py-1.5 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-inset focus:ring-sky-700 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="flex w-full justify-center rounded-md bg-sky-700 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Sign in</button>
|
||||
<button type="submit" class="flex w-full justify-center rounded-md bg-sky-700 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Sign in</button>
|
||||
</div>
|
||||
</form>
|
||||
<% } %>
|
||||
@@ -79,7 +79,7 @@
|
||||
<% } %>
|
||||
|
||||
<div class="<%= internalAuth ? 'mt-6' : '' %>">
|
||||
<a href="<%= baseUrl %>/oidc/login" class="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent">
|
||||
<a href="<%= baseUrl %>/oidc/login" class="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent">
|
||||
<svg class="h-5 w-5" viewBox="0 0 64 64" aria-hidden="true">
|
||||
<path fill="#f7931e" d="M29.1 6.4v54.5l9.7-4.5V1.7l-9.7 4.7z"></path>
|
||||
<path fill="#b2b2b2" d="M62.7 22.4L64 36.3l-18.7-4.1"></path>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<nav class="sticky top-0 z-40 bg-white shadow-sm dark:bg-gray-800">
|
||||
<nav class="sticky top-0 z-40 bg-white shadow-xs dark:bg-gray-800">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 justify-between">
|
||||
<div class="flex">
|
||||
<% if(uiBackButton) { %>
|
||||
<button aria-label="Back to Previous Page" onclick="window.history.go(-1);">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="text-gray-900 dark:text-white hover:text-gray-700 hover:dark:text-gray-100 w-10 h-10 mr-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-100 w-10 h-10 mr-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-4.28 9.22a.75.75 0 0 0 0 1.06l3 3a.75.75 0 1 0 1.06-1.06l-1.72-1.72h5.69a.75.75 0 0 0 0-1.5h-5.69l1.72-1.72a.75.75 0 0 0-1.06-1.06l-3 3Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<% } %>
|
||||
<a href="<%= baseUrl %>/vouchers" class="flex flex-shrink-0 items-center">
|
||||
<a href="<%= baseUrl %>/vouchers" class="flex shrink-0 items-center">
|
||||
<img class="h-12 w-auto" width="48" height="48" alt="UniFi Voucher Site Logo" src="<%= baseUrl %>/images/logo.png">
|
||||
<div class="hidden sm:block ml-4 text-2xl font-semibold leading-7 text-gray-900 dark:text-white">
|
||||
UniFi Voucher
|
||||
@@ -18,8 +18,8 @@
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<% if(new_voucher_button) { %>
|
||||
<div class="flex-shrink-0">
|
||||
<button id="create-button-header" type="button" class="relative inline-flex items-center gap-x-1.5 rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
|
||||
<div class="shrink-0">
|
||||
<button id="create-button-header" type="button" class="relative inline-flex items-center gap-x-1.5 rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
|
||||
<svg class="-ml-0.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
||||
</svg>
|
||||
@@ -27,9 +27,9 @@
|
||||
</button>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="ml-4 flex flex-shrink-0 items-center">
|
||||
<div class="ml-4 flex shrink-0 items-center">
|
||||
<div class="relative">
|
||||
<button id="user-menu-button" class="flex items-center focus:outline-none focus:ring-2 focus:ring-sky-600 rounded-full">
|
||||
<button id="user-menu-button" class="flex items-center focus:outline-hidden focus:ring-2 focus:ring-sky-600 rounded-full">
|
||||
<span class="absolute -inset-1.5"></span>
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<% if(userIcon !== '') { %>
|
||||
@@ -40,7 +40,7 @@
|
||||
</svg>
|
||||
<% } %>
|
||||
</button>
|
||||
<div id="user-dropdown" class="hidden absolute bg-white dark:bg-gray-800 right-0 mt-2 w-64 rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5">
|
||||
<div id="user-dropdown" class="hidden absolute bg-white dark:bg-gray-800 right-0 mt-2 w-64 rounded-md shadow-lg py-1 ring-1 ring-black/25">
|
||||
<div class="px-4 py-2 text-sm text-gray-600 dark:text-gray-400 border-b border-black/5 dark:border-white/5">
|
||||
Logged in as:<br>
|
||||
<span class="font-medium"><%= user.email %></span>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<div class="mx-6">
|
||||
<div class="max-w-7xl mx-auto mt-5 rounded-md bg-red-700 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<svg class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
@@ -52,7 +52,7 @@
|
||||
<div class="mx-6">
|
||||
<div class="max-w-7xl mx-auto mt-5 rounded-md bg-green-700 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<svg class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
@@ -72,7 +72,7 @@
|
||||
<div class="flex flex-col md:flex-row md:items-end md:space-x-4 space-y-2 md:space-y-0">
|
||||
<div class="flex flex-col">
|
||||
<label for="status" class="text-xs text-gray-900 dark:text-white mb-1">Status</label>
|
||||
<select id="status" name="status" class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 [&_*]:text-black" aria-label="Filter by status">
|
||||
<select id="status" name="status" class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black" aria-label="Filter by status">
|
||||
<option value="all"<%= filters.status === 'all' ? ' selected' : '' %>>All</option>
|
||||
<option value="available"<%= filters.status === 'available' ? ' selected' : '' %>>Available</option>
|
||||
<option value="in-use"<%= filters.status === 'in-use' ? ' selected' : '' %>>In Use</option>
|
||||
@@ -81,7 +81,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label for="quota" class="text-xs text-gray-900 dark:text-white mb-1">Quota</label>
|
||||
<select id="quota" name="quota" class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 [&_*]:text-black" aria-label="Filter by quota">
|
||||
<select id="quota" name="quota" class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black" aria-label="Filter by quota">
|
||||
<option value="all"<%= filters.quota === 'all' ? ' selected' : '' %>>All</option>
|
||||
<option value="multi-use"<%= filters.quota === 'multi-use' ? ' selected' : '' %>>Multi-use</option>
|
||||
<option value="single-use"<%= filters.quota === 'single-use' ? ' selected' : '' %>>Single-use</option>
|
||||
@@ -89,7 +89,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label for="sort" class="text-xs text-gray-900 dark:text-white mb-1">Sort</label>
|
||||
<select id="sort" name="sort" class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 [&_*]:text-black" aria-label="Sort by">
|
||||
<select id="sort" name="sort" class="rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black" aria-label="Sort by">
|
||||
<option value="date"<%= sort === 'date' ? ' selected' : '' %>>Date</option>
|
||||
<option value="code"<%= sort === 'code' ? ' selected' : '' %>>Code</option>
|
||||
<option value="note"<%= sort === 'note' ? ' selected' : '' %>>Notes</option>
|
||||
@@ -106,7 +106,7 @@
|
||||
<span class="mb-1 text-xs text-gray-900 dark:text-white<%= !printer_enabled ? ' hidden' : '' %>">
|
||||
|
||||
</span>
|
||||
<button id="bulk-print" class="w-fit justify-self-end relative inline-flex items-center gap-x-1.5 rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700<%= !printer_enabled ? ' hidden' : '' %>">
|
||||
<button id="bulk-print" class="<%= !printer_enabled ? 'hidden' : 'inline-flex' %> w-fit justify-self-end relative items-center gap-x-1.5 rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
|
||||
<svg class="-ml-0.5 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z" />
|
||||
</svg>
|
||||
@@ -117,7 +117,7 @@
|
||||
<span class="mb-1 text-xs text-gray-900 dark:text-white">
|
||||
Last Sync: <%= new Intl.DateTimeFormat('en-GB', {day: "numeric", month: "numeric", hour: "numeric", minute: "numeric", hour12: false}).format(new Date(updated)) %>
|
||||
</span>
|
||||
<a href="<%= baseUrl %>/vouchers?refresh=true" id="reload-vouchers" type="button" class="w-fit justify-self-end relative inline-flex items-center gap-x-1.5 rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
|
||||
<a href="<%= baseUrl %>/vouchers?refresh=true" id="reload-vouchers" type="button" class="w-fit justify-self-end relative inline-flex items-center gap-x-1.5 rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
|
||||
<svg class="-ml-0.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
@@ -133,7 +133,7 @@
|
||||
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">No vouchers</h3>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">Get started by creating a new voucher.</p>
|
||||
<div class="mt-6">
|
||||
<button id="create-button-info" type="button" class="inline-flex items-center rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
|
||||
<button id="create-button-info" type="button" class="inline-flex items-center rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">
|
||||
<svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
||||
</svg>
|
||||
@@ -243,7 +243,7 @@
|
||||
<div id="bulk-print-dialog"></div>
|
||||
|
||||
<div id="create-dialog" class="hidden relative z-40" role="dialog" aria-modal="true">
|
||||
<div id="create-dialog-overlay" class="fixed inset-0 bg-gray-500 bg-opacity-75"></div>
|
||||
<div id="create-dialog-overlay" class="fixed inset-0 bg-gray-500/75"></div>
|
||||
|
||||
<div class="fixed overflow-hidden">
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
@@ -265,7 +265,7 @@
|
||||
<div>
|
||||
<label for="voucher-type" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Preset Voucher Type</label>
|
||||
<div class="mt-2">
|
||||
<select id="voucher-type" name="voucher-type" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 [&_*]:text-black">
|
||||
<select id="voucher-type" name="voucher-type" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
|
||||
<% voucher_types.forEach((type) => { %>
|
||||
<option value="<%= type.raw %>"><%= timeConvert(type.expiration) %>, <%= type.usage === '1' ? 'single-use' : type.usage === '0' ? 'multi-use (unlimited)' : `multi-use (${type.usage}x)` %><%= typeof type.upload === "undefined" && typeof type.download === "undefined" && typeof type.megabytes === "undefined" ? ', no limits' : '' %><%= typeof type.upload !== "undefined" ? `, upload bandwidth limit: ${type.upload} kb/s` : '' %><%= typeof type.download !== "undefined" ? `, download bandwidth limit: ${type.download} kb/s` : '' %><%= typeof type.megabytes !== "undefined" ? `, quota limit: ${type.megabytes} mb` : '' %></option>
|
||||
<% }); %>
|
||||
@@ -291,7 +291,7 @@
|
||||
<label for="voucher-duration" class="mt-4 block text-sm font-medium leading-6 text-gray-900 dark:text-white">Duration</label>
|
||||
<div class="mt-2 grid grid-cols-2 gap-2">
|
||||
<input type="number" min="1" step="1" value="8" id="voucher-duration" name="voucher-duration" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6">
|
||||
<select id="voucher-duration-type" name="voucher-duration-type" class="block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 [&_*]:text-black">
|
||||
<select id="voucher-duration-type" name="voucher-duration-type" class="block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
|
||||
<option value="minute">Minute(s)</option>
|
||||
<option value="hour" selected>Hour(s)</option>
|
||||
<option value="day">Day(s)</option>
|
||||
@@ -301,7 +301,7 @@
|
||||
<div class="custom-voucher-field hidden">
|
||||
<label for="voucher-usage" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Voucher Usage</label>
|
||||
<div class="mt-2">
|
||||
<select id="voucher-usage" name="voucher-usage" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 [&_*]:text-black">
|
||||
<select id="voucher-usage" name="voucher-usage" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
|
||||
<option value="0">Multi-use (Unlimited)</option>
|
||||
<option value="-1">Multi-use (Limited/Quota)</option>
|
||||
<option value="1">Single-use</option>
|
||||
@@ -336,9 +336,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-shrink-0 justify-end px-4 py-4">
|
||||
<button id="cancel" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" class="ml-4 inline-flex justify-center rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Create</button>
|
||||
<div class="flex shrink-0 justify-end px-4 py-4">
|
||||
<button id="cancel" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" class="ml-4 inline-flex justify-center rounded-md bg-sky-700 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-sky-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -393,10 +393,10 @@
|
||||
|
||||
<div aria-live="assertive" class="z-40 pointer-events-none fixed inset-0 flex items-end px-4 py-6">
|
||||
<div id="copy-notification" style="display: none;" class="flex w-full flex-col items-center space-y-4">
|
||||
<div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5">
|
||||
<div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5">
|
||||
<div class="p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
<svg class="h-6 w-6 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user