forked from vikunja/frontend
Compare commits
53 Commits
renovate/p
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
5ac1b5919a | ||
6fad1e4969 | |||
eaeddda4e4 | |||
7cbf0acac5 | |||
3db5ea45d7 | |||
|
dcd5c3fd6a | ||
61fff44764 | |||
ecdae4e03e | |||
b26ea45fe0 | |||
7cb0cd293d | |||
6572f75e5d | |||
af55992057 | |||
e92559dc00 | |||
3dbf02fd7a | |||
81a4f2d977 | |||
2972d0d400 | |||
c11ebc44c4 | |||
144f90c5f7 | |||
913879604a | |||
1589ed5739 | |||
a991c537ac | |||
69b57aa23a | |||
1a1939963a | |||
3d62c9789c | |||
c18df8687c | |||
d83ba0c158 | |||
cea31d1da7 | |||
12509a7e0f | |||
dd43057a08 | |||
19d3cf01cd | |||
80012bf035 | |||
899d9e1cb7 | |||
56830ddadc | |||
1749d6ba0a | |||
b29008d304 | |||
8ae3054b1a | |||
f9dad79b23 | |||
30f5cb0656 | |||
3f58c983da | |||
8fa8b03aa6 | |||
e4499f44b7 | |||
b799233bca | |||
be0ae4bc29 | |||
60d99f3bba | |||
fa666d2817 | |||
9312aa14fa | |||
68e4f776b9 | |||
2d137d564e | |||
|
fc8824d942 | ||
6d4ca57601 | |||
d2bf4e38b1 | |||
a5f6857a40 | |||
ed3d79fa4c |
|
@ -1,8 +1,13 @@
|
||||||
# Duplicate this file and remove the '.example' suffix.
|
# (1) Duplicate this file and remove the '.example' suffix.
|
||||||
# Adjust the values as needed.
|
# Naming this file '.env.local' is a Vite convention to prevent accidentally
|
||||||
|
# submitting to git.
|
||||||
|
# For more info see: https://vitejs.dev/guide/env-and-mode.html#env-files
|
||||||
|
|
||||||
VITE_IS_ONLINE=true
|
# (2) Comment in and adjust the values as needed.
|
||||||
VITE_WORKBOX_DEBUG=false
|
|
||||||
SENTRY_AUTH_TOKEN=YOUR_TOKEN
|
# VITE_IS_ONLINE=true
|
||||||
SENTRY_ORG=vikunja
|
# VITE_WORKBOX_DEBUG=false
|
||||||
SENTRY_PROJECT=frontend-oss
|
# SENTRY_AUTH_TOKEN=YOUR_TOKEN
|
||||||
|
# SENTRY_ORG=vikunja
|
||||||
|
# SENTRY_PROJECT=frontend-oss
|
||||||
|
# VIKUNJA_FRONTEND_BASE=/custom-subpath
|
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
|
@ -1,2 +1,3 @@
|
||||||
github: kolaente
|
github: kolaente
|
||||||
custom: https://www.buymeacoffee.com/kolaente
|
open_collective: vikunja
|
||||||
|
custom: ["https://vikunja.cloud", "https://www.buymeacoffee.com/kolaente"]
|
||||||
|
|
89
Dockerfile
89
Dockerfile
|
@ -1,49 +1,70 @@
|
||||||
# Stage 1: Build application
|
# syntax=docker/dockerfile:1
|
||||||
FROM --platform=$BUILDPLATFORM node:18-alpine AS compile-image
|
# ┬─┐┬ ┐o┬ ┬─┐
|
||||||
|
# │─││ │││ │ │
|
||||||
|
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||||
|
|
||||||
|
FROM --platform=$BUILDPLATFORM node:18-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
ARG USE_RELEASE=false
|
ARG USE_RELEASE=false
|
||||||
ARG RELEASE_VERSION=main
|
ARG RELEASE_VERSION=main
|
||||||
|
|
||||||
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
||||||
ADD . ./
|
|
||||||
|
|
||||||
RUN \
|
COPY package.json ./
|
||||||
if [ $USE_RELEASE = true ]; then \
|
COPY pnpm-lock.yaml ./
|
||||||
wget https://dl.vikunja.io/frontend/vikunja-frontend-$RELEASE_VERSION.zip -O frontend-release.zip && \
|
|
||||||
unzip frontend-release.zip -d dist/ && \
|
|
||||||
exit 0; \
|
|
||||||
fi && \
|
|
||||||
# https://pnpm.io/installation#using-corepack
|
|
||||||
corepack enable && \
|
|
||||||
# we don't use corepack prepare here by intend since
|
|
||||||
# we have renovate to keep our dependencies up to date
|
|
||||||
# Build the frontend
|
|
||||||
pnpm install && \
|
|
||||||
apk add --no-cache git && \
|
|
||||||
echo '{"VERSION": "'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'"}' > src/version.json && \
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# Stage 2: copy
|
RUN if [ "$USE_RELEASE" != true ]; then \
|
||||||
FROM nginx:alpine
|
# https://pnpm.io/installation#using-corepack
|
||||||
|
corepack enable && \
|
||||||
|
pnpm install; \
|
||||||
|
fi
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
COPY . ./
|
||||||
COPY scripts/run.sh /run.sh
|
|
||||||
|
|
||||||
# copy compiled files from stage 1
|
RUN if [ "$USE_RELEASE" != true ]; then \
|
||||||
COPY --from=compile-image /build/dist /usr/share/nginx/html
|
apk add --no-cache --virtual .build-deps git jq && \
|
||||||
|
git describe --tags --always --abbrev=10 | sed 's/-/+/; s/^v//; s/-g/-/' | \
|
||||||
|
xargs -0 -I{} jq -Mcnr --arg version {} '{VERSION:$version}' | \
|
||||||
|
tee src/version.json && \
|
||||||
|
apk del .build-deps; \
|
||||||
|
fi
|
||||||
|
|
||||||
# Unprivileged user
|
RUN if [ "$USE_RELEASE" = true ]; then \
|
||||||
ENV PUID 1000
|
wget "https://dl.vikunja.io/frontend/vikunja-frontend-${RELEASE_VERSION}.zip" -O frontend-release.zip && \
|
||||||
ENV PGID 1000
|
unzip frontend-release.zip -d dist/; \
|
||||||
|
else \
|
||||||
|
# we don't use corepack prepare here by intend since
|
||||||
|
# we have renovate to keep our dependencies up to date
|
||||||
|
# Build the frontend
|
||||||
|
pnpm run build; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ┌┐┐┌─┐o┌┐┐┐ │
|
||||||
|
# ││││ ┬││││┌┼┘
|
||||||
|
# ┘└┘┘─┘┘┘└┘┘ └
|
||||||
|
|
||||||
|
FROM nginx:stable-alpine AS runner
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
LABEL maintainer="maintainers@vikunja.io"
|
LABEL maintainer="maintainers@vikunja.io"
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
ENV VIKUNJA_HTTP_PORT 80
|
||||||
# for sh file
|
ENV VIKUNJA_HTTP2_PORT 81
|
||||||
bash \
|
ENV VIKUNJA_LOG_FORMAT main
|
||||||
# installs usermod and groupmod
|
ENV VIKUNJA_API_URL http://localhost:3456/api/v1
|
||||||
shadow
|
ENV VIKUNJA_SENTRY_ENABLED false
|
||||||
|
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
|
||||||
|
|
||||||
CMD "/run.sh"
|
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
|
||||||
|
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh
|
||||||
|
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY docker/templates/. /etc/nginx/templates/
|
||||||
|
# copy compiled files from stage 1
|
||||||
|
COPY --from=builder /build/dist ./
|
||||||
|
# manage permissions
|
||||||
|
RUN chmod 0755 /docker-entrypoint.d/*.sh /etc/nginx/templates && \
|
||||||
|
chmod -R 0644 /etc/nginx/nginx.conf && \
|
||||||
|
chown -R nginx:nginx ./ /etc/nginx/conf.d /etc/nginx/templates && \
|
||||||
|
rm -f /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
|
||||||
|
# unprivileged user
|
||||||
|
USER nginx
|
||||||
|
|
|
@ -18,6 +18,14 @@ If you find any security-related issues you don't want to disclose publicly, ple
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
|
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
|
||||||
|
In order to build it from sources run the command below. (Docker >= v19.03)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export DOCKER_BUILDKIT=1
|
||||||
|
docker build -t vikunja/frontend .
|
||||||
|
```
|
||||||
|
|
||||||
|
Refer to Refer [to multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for the different platform.
|
||||||
|
|
||||||
## Project setup
|
## Project setup
|
||||||
|
|
||||||
|
|
15
docker/injector.sh
Normal file
15
docker/injector.sh
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "info: API URL is $VIKUNJA_API_URL"
|
||||||
|
echo "info: Sentry enabled: $VIKUNJA_SENTRY_ENABLED"
|
||||||
|
|
||||||
|
# Escape the variable to prevent sed from complaining
|
||||||
|
VIKUNJA_API_URL="$(echo "$VIKUNJA_API_URL" | sed -r 's/([:;])/\\\1/g')"
|
||||||
|
VIKUNJA_SENTRY_DSN="$(echo "$VIKUNJA_SENTRY_DSN" | sed -r 's/([:;])/\\\1/g')"
|
||||||
|
|
||||||
|
sed -ri "s:^(\s*window.API_URL\s*=)\s*.+:\1 '${VIKUNJA_API_URL}':g" /usr/share/nginx/html/index.html
|
||||||
|
sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g" /usr/share/nginx/html/index.html
|
||||||
|
sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
|
||||||
|
|
||||||
|
date -uIseconds | xargs echo 'info: started at'
|
19
docker/ipv6-disable.sh
Normal file
19
docker/ipv6-disable.sh
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ ! -f "/proc/net/if_inet6" ]; then
|
||||||
|
echo "info: IPv6 is not available! Removing IPv6 listen configuration"
|
||||||
|
find /etc/nginx/conf.d -name '*.conf' -type f | \
|
||||||
|
while IFS= read -r CONFIG; do
|
||||||
|
sed -r '/^\s*listen\s*\[::\]:.+$/d' "$CONFIG" > "$CONFIG.temp"
|
||||||
|
if ! diff -U 5 "$CONFIG" "$CONFIG.temp" > "$CONFIG.diff"; then
|
||||||
|
echo "info: Removing IPv6 lines from $CONFIG" | \
|
||||||
|
cat - "$CONFIG.diff"
|
||||||
|
echo "# IPv6 is disabled because /proc/net/if_inet6 was not found" | \
|
||||||
|
cat - "$CONFIG.temp" > "$CONFIG"
|
||||||
|
else
|
||||||
|
echo "info: Skipping $CONFIG because it does not have IPv6 listen"
|
||||||
|
fi
|
||||||
|
rm -f "$CONFIG.temp" "$CONFIG.diff"
|
||||||
|
done
|
||||||
|
fi
|
112
docker/nginx.conf
Normal file
112
docker/nginx.conf
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
# Generated by nginxconfig.io
|
||||||
|
# https://www.digitalocean.com/community/tools/nginx?domains.0.server.domain=localhost&domains.0.server.documentRoot=%2Fusr%2Fshare%2Fnginx%2Fhtml&domains.0.server.cdnSubdomain=true&domains.0.https.https=false&domains.0.php.php=false&domains.0.routing.index=index.html&domains.0.routing.fallbackHtml=true&domains.0.routing.fallbackPhp=false&global.performance.assetsExpiration=1d&global.performance.mediaExpiration=1d&global.performance.svgExpiration=1d&global.performance.fontsExpiration=1d&global.logging.accessLog=%2Fdev%2Fstdout&global.logging.errorLog=%2Fdev%2Fstderr%20warn&global.logging.logNotFound=true&global.nginx.user=nginx&global.nginx.pid=%2Fvar%2Frun%2Fnginx.pid&global.nginx.clientMaxBodySize=50&global.docker.dockerfile=true&global.tools.modularizedStructure=false&global.tools.symlinkVhost=false
|
||||||
|
# and then edited manually ;)
|
||||||
|
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
worker_processes auto;
|
||||||
|
worker_rlimit_nofile 65535;
|
||||||
|
|
||||||
|
events {
|
||||||
|
multi_accept on;
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
charset utf-8;
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
server_tokens off;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
types_hash_bucket_size 64;
|
||||||
|
|
||||||
|
# rootless
|
||||||
|
client_body_temp_path /tmp/client_temp;
|
||||||
|
proxy_temp_path /tmp/proxy_temp_path;
|
||||||
|
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||||
|
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||||
|
scgi_temp_path /tmp/scgi_temp;
|
||||||
|
|
||||||
|
# MIME
|
||||||
|
include mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
types {
|
||||||
|
application/manifest+json webmanifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log_format json escape=json
|
||||||
|
'{'
|
||||||
|
'"bytes_sent": "$bytes_sent",'
|
||||||
|
'"http_user_agent": "$http_user_agent",'
|
||||||
|
'"nginx_version": "$nginx_version",'
|
||||||
|
'"query_string": "$query_string",'
|
||||||
|
'"realip_remote_addr": "$realip_remote_addr",'
|
||||||
|
'"remote_addr": "$remote_addr",'
|
||||||
|
'"remote_user": "$remote_user",'
|
||||||
|
'"request_length": "$request_length",'
|
||||||
|
'"request_method": "$request_method",'
|
||||||
|
'"request_time": "$request_time",'
|
||||||
|
'"server_addr": "$server_addr",'
|
||||||
|
'"server_port": "$server_port",'
|
||||||
|
'"server_protocol": "$server_protocol",'
|
||||||
|
'"status": "$status",'
|
||||||
|
'"time_local": "$time_local",'
|
||||||
|
'"uri": "$uri"'
|
||||||
|
'}';
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /dev/stdout main;
|
||||||
|
error_log /dev/stderr warn;
|
||||||
|
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
# compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_buffers 16 8k;
|
||||||
|
gzip_http_version 1.1;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
application/json
|
||||||
|
application/x-javascript
|
||||||
|
application/javascript
|
||||||
|
text/xml
|
||||||
|
application/xml
|
||||||
|
application/xml+rss
|
||||||
|
text/javascript
|
||||||
|
application/vnd.ms-fontobject
|
||||||
|
application/x-font-ttf
|
||||||
|
font/opentype
|
||||||
|
image/svg+xml
|
||||||
|
image/x-icon
|
||||||
|
audio/wav;
|
||||||
|
|
||||||
|
map_hash_max_size 128;
|
||||||
|
map_hash_bucket_size 128;
|
||||||
|
|
||||||
|
map $sent_http_content_type $expires {
|
||||||
|
default off;
|
||||||
|
text/css max;
|
||||||
|
application/javascript max;
|
||||||
|
text/javascript max;
|
||||||
|
application/vnd.ms-fontobject max;
|
||||||
|
application/x-font-ttf max;
|
||||||
|
font/opentype max;
|
||||||
|
font/woff2 max;
|
||||||
|
image/svg+xml max;
|
||||||
|
image/x-icon max;
|
||||||
|
audio/wav max;
|
||||||
|
~images/ max;
|
||||||
|
~font/ max;
|
||||||
|
}
|
||||||
|
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
71
docker/templates/default.conf.template
Normal file
71
docker/templates/default.conf.template
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
server {
|
||||||
|
listen ${VIKUNJA_HTTP_PORT};
|
||||||
|
listen [::]:${VIKUNJA_HTTP_PORT};
|
||||||
|
## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
||||||
|
listen ${VIKUNJA_HTTP2_PORT} default_server http2 proxy_protocol;
|
||||||
|
listen [::]:${VIKUNJA_HTTP2_PORT} default_server http2 proxy_protocol;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
expires $expires;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
access_log /dev/stdout ${VIKUNJA_LOG_FORMAT};
|
||||||
|
# security headers
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self' http: https: ws: wss: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always;
|
||||||
|
add_header Permissions-Policy "interest-cohort=()" always;
|
||||||
|
|
||||||
|
# . files
|
||||||
|
location ~ /\.(?!well-known) {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# assume that everything else is handled by the application router, by injecting the index.html.
|
||||||
|
location / {
|
||||||
|
autoindex off;
|
||||||
|
expires off;
|
||||||
|
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
||||||
|
try_files $uri /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# favicon.ico
|
||||||
|
location = /favicon.ico {
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# robots.txt
|
||||||
|
location = /robots.txt {
|
||||||
|
log_not_found off;
|
||||||
|
access_log off;
|
||||||
|
expires -1; # no-cache
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /ready {
|
||||||
|
return 200 "";
|
||||||
|
access_log off;
|
||||||
|
expires -1; # no-cache
|
||||||
|
}
|
||||||
|
|
||||||
|
# all assets contain hash in filename, cache forever
|
||||||
|
location ^~ /assets/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# all workbox scripts are compiled with hash in filename, cache forever3
|
||||||
|
location ^~ /workbox- {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# assets, media
|
||||||
|
location ~* .(txt|webmanifest|css|js|mjs|map|svg|jpg|jpeg|png|ico|ttf|woff|woff2|wav)$ {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html { }
|
||||||
|
|
||||||
|
}
|
117
nginx.conf
117
nginx.conf
|
@ -1,117 +0,0 @@
|
||||||
user nginx;
|
|
||||||
worker_processes 1;
|
|
||||||
|
|
||||||
error_log /var/log/nginx/error.log warn;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
types {
|
|
||||||
application/manifest+json webmanifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
|
|
||||||
sendfile on;
|
|
||||||
#tcp_nopush on;
|
|
||||||
|
|
||||||
keepalive_timeout 65;
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_buffers 16 8k;
|
|
||||||
gzip_http_version 1.1;
|
|
||||||
gzip_min_length 256;
|
|
||||||
gzip_types
|
|
||||||
text/plain
|
|
||||||
text/css
|
|
||||||
application/json
|
|
||||||
application/x-javascript
|
|
||||||
application/javascript
|
|
||||||
text/xml
|
|
||||||
application/xml
|
|
||||||
application/xml+rss
|
|
||||||
text/javascript
|
|
||||||
application/vnd.ms-fontobject
|
|
||||||
application/x-font-ttf
|
|
||||||
font/opentype
|
|
||||||
image/svg+xml
|
|
||||||
image/x-icon
|
|
||||||
audio/wav;
|
|
||||||
|
|
||||||
map_hash_max_size 128;
|
|
||||||
map_hash_bucket_size 128;
|
|
||||||
|
|
||||||
# Expires map
|
|
||||||
map $sent_http_content_type $expires {
|
|
||||||
default off;
|
|
||||||
text/css max;
|
|
||||||
application/javascript max;
|
|
||||||
text/javascript max;
|
|
||||||
application/vnd.ms-fontobject max;
|
|
||||||
application/x-font-ttf max;
|
|
||||||
font/opentype max;
|
|
||||||
font/woff2 max;
|
|
||||||
image/svg+xml max;
|
|
||||||
image/x-icon max;
|
|
||||||
audio/wav max;
|
|
||||||
~images/ max;
|
|
||||||
~font/ max;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
listen 81 default_server http2 proxy_protocol; ## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
|
||||||
listen [::]:81 default_server http2 proxy_protocol; ## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
|
||||||
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
expires $expires;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
|
|
||||||
# all assets contain hash in filename, cache forever
|
|
||||||
location ^~ /assets/ {
|
|
||||||
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# all workbox scripts are compiled with hash in filename, cache forever3
|
|
||||||
location ^~ /workbox- {
|
|
||||||
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# assume that everything else is handled by the application router, by injecting the index.html.
|
|
||||||
location / {
|
|
||||||
autoindex off;
|
|
||||||
expires off;
|
|
||||||
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
|
||||||
try_files $uri /index.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* .(txt|webmanifest|css|js|mjs|map|svg|jpg|jpeg|png|ico|ttf|woff|woff2|wav)$ {
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
|
||||||
location = /50x.html {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
48
package.json
48
package.json
|
@ -13,7 +13,7 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://vikunja.io/",
|
"homepage": "https://vikunja.io/",
|
||||||
"funding": "https://opencollective.com/vikunja",
|
"funding": "https://opencollective.com/vikunja",
|
||||||
"packageManager": "pnpm@7.26.0",
|
"packageManager": "pnpm@7.26.3",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"todo",
|
"todo",
|
||||||
"productivity",
|
"productivity",
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||||
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
|
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
|
||||||
"test:e2e-dev": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
|
"test:e2e-dev": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest --dir ./src",
|
||||||
"typecheck": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
"typecheck": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||||
"browserslist:update": "pnpm dlx browserslist@latest --update-db",
|
"browserslist:update": "pnpm dlx browserslist@latest --update-db",
|
||||||
"fonts:update": "pnpm fonts:download && pnpm fonts:subset",
|
"fonts:update": "pnpm fonts:download && pnpm fonts:subset",
|
||||||
|
@ -53,13 +53,13 @@
|
||||||
"@infectoone/vue-ganttastic": "2.1.4",
|
"@infectoone/vue-ganttastic": "2.1.4",
|
||||||
"@intlify/unplugin-vue-i18n": "0.8.1",
|
"@intlify/unplugin-vue-i18n": "0.8.1",
|
||||||
"@kyvg/vue3-notification": "2.8.0",
|
"@kyvg/vue3-notification": "2.8.0",
|
||||||
"@sentry/tracing": "7.34.0",
|
"@sentry/tracing": "7.36.0",
|
||||||
"@sentry/vue": "7.34.0",
|
"@sentry/vue": "7.36.0",
|
||||||
"@types/is-touch-device": "1.0.0",
|
"@types/is-touch-device": "1.0.0",
|
||||||
"@types/lodash.clonedeep": "4.5.7",
|
"@types/lodash.clonedeep": "4.5.7",
|
||||||
"@types/sortablejs": "1.15.0",
|
"@types/sortablejs": "1.15.0",
|
||||||
"@vueuse/core": "9.11.1",
|
"@vueuse/core": "9.12.0",
|
||||||
"axios": "1.2.5",
|
"axios": "1.3.2",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"bulma-css-variables": "0.9.33",
|
"bulma-css-variables": "0.9.33",
|
||||||
"camel-case": "4.1.2",
|
"camel-case": "4.1.2",
|
||||||
|
@ -79,12 +79,12 @@
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"marked": "4.2.12",
|
"marked": "4.2.12",
|
||||||
"minimist": "1.2.7",
|
"minimist": "1.2.7",
|
||||||
"pinia": "2.0.29",
|
"pinia": "2.0.30",
|
||||||
"register-service-worker": "1.7.2",
|
"register-service-worker": "1.7.2",
|
||||||
"snake-case": "3.0.4",
|
"snake-case": "3.0.4",
|
||||||
"sortablejs": "1.15.0",
|
"sortablejs": "1.15.0",
|
||||||
"ufo": "1.0.1",
|
"ufo": "1.0.1",
|
||||||
"vue": "3.2.45",
|
"vue": "3.2.47",
|
||||||
"vue-advanced-cropper": "2.8.8",
|
"vue-advanced-cropper": "2.8.8",
|
||||||
"vue-flatpickr-component": "11.0.1",
|
"vue-flatpickr-component": "11.0.1",
|
||||||
"vue-i18n": "9.2.2",
|
"vue-i18n": "9.2.2",
|
||||||
|
@ -108,38 +108,38 @@
|
||||||
"@types/marked": "4.0.8",
|
"@types/marked": "4.0.8",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
"@types/postcss-preset-env": "7.7.0",
|
"@types/postcss-preset-env": "7.7.0",
|
||||||
"@typescript-eslint/eslint-plugin": "5.49.0",
|
"@typescript-eslint/eslint-plugin": "5.50.0",
|
||||||
"@typescript-eslint/parser": "5.49.0",
|
"@typescript-eslint/parser": "5.50.0",
|
||||||
"@vitejs/plugin-legacy": "3.0.2",
|
"@vitejs/plugin-legacy": "4.0.1",
|
||||||
"@vitejs/plugin-vue": "4.0.0",
|
"@vitejs/plugin-vue": "4.0.0",
|
||||||
"@vue/eslint-config-typescript": "11.0.2",
|
"@vue/eslint-config-typescript": "11.0.2",
|
||||||
"@vue/test-utils": "2.2.7",
|
"@vue/test-utils": "2.2.10",
|
||||||
"@vue/tsconfig": "0.1.3",
|
"@vue/tsconfig": "0.1.3",
|
||||||
"autoprefixer": "10.4.13",
|
"autoprefixer": "10.4.13",
|
||||||
"browserslist": "4.21.4",
|
"browserslist": "4.21.4",
|
||||||
"caniuse-lite": "1.0.30001447",
|
"caniuse-lite": "1.0.30001449",
|
||||||
"csstype": "3.1.1",
|
"csstype": "3.1.1",
|
||||||
"cypress": "12.4.0",
|
"cypress": "12.5.1",
|
||||||
"esbuild": "0.17.4",
|
"esbuild": "0.17.5",
|
||||||
"eslint": "8.32.0",
|
"eslint": "8.33.0",
|
||||||
"eslint-plugin-vue": "9.9.0",
|
"eslint-plugin-vue": "9.9.0",
|
||||||
"happy-dom": "8.1.5",
|
"happy-dom": "8.2.0",
|
||||||
"histoire": "0.12.4",
|
"histoire": "0.12.4",
|
||||||
"netlify-cli": "12.9.1",
|
"netlify-cli": "12.10.0",
|
||||||
"postcss": "8.4.21",
|
"postcss": "8.4.21",
|
||||||
"postcss-easing-gradients": "3.0.1",
|
"postcss-easing-gradients": "3.0.1",
|
||||||
"postcss-easings": "3.0.1",
|
"postcss-easings": "3.0.1",
|
||||||
"postcss-preset-env": "7.8.3",
|
"postcss-preset-env": "8.0.1",
|
||||||
"rollup": "3.11.0",
|
"rollup": "3.13.0",
|
||||||
"rollup-plugin-visualizer": "5.9.0",
|
"rollup-plugin-visualizer": "5.9.0",
|
||||||
"sass": "1.57.1",
|
"sass": "1.58.0",
|
||||||
"start-server-and-test": "1.15.3",
|
"start-server-and-test": "1.15.3",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.5",
|
||||||
"vite": "4.0.4",
|
"vite": "4.1.1",
|
||||||
"vite-plugin-inject-preload": "1.2.0",
|
"vite-plugin-inject-preload": "1.2.0",
|
||||||
"vite-plugin-pwa": "0.14.1",
|
"vite-plugin-pwa": "0.14.1",
|
||||||
"vite-svg-loader": "4.0.0",
|
"vite-svg-loader": "4.0.0",
|
||||||
"vitest": "0.28.2",
|
"vitest": "0.28.4",
|
||||||
"vue-tsc": "1.0.24",
|
"vue-tsc": "1.0.24",
|
||||||
"wait-on": "7.0.1",
|
"wait-on": "7.0.1",
|
||||||
"workbox-cli": "6.5.4"
|
"workbox-cli": "6.5.4"
|
||||||
|
|
2754
pnpm-lock.yaml
2754
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,28 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# This shell script sets the api url based on an environment variable and starts nginx in foreground.
|
|
||||||
|
|
||||||
VIKUNJA_API_URL="${VIKUNJA_API_URL:-"/api/v1"}"
|
|
||||||
VIKUNJA_SENTRY_ENABLED="${VIKUNJA_SENTRY_ENABLED:-"false"}"
|
|
||||||
VIKUNJA_SENTRY_DSN="${VIKUNJA_SENTRY_DSN:-"https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480"}"
|
|
||||||
VIKUNJA_HTTP_PORT="${VIKUNJA_HTTP_PORT:-80}"
|
|
||||||
VIKUNJA_HTTPS_PORT="${VIKUNJA_HTTPS_PORT:-443}"
|
|
||||||
|
|
||||||
echo "Using $VIKUNJA_API_URL as default api url"
|
|
||||||
|
|
||||||
# Escape the variable to prevent sed from complaining
|
|
||||||
VIKUNJA_API_URL=$(echo $VIKUNJA_API_URL |sed 's/\//\\\//g')
|
|
||||||
|
|
||||||
sed -i "s/http\:\/\/localhost\:3456//g" /usr/share/nginx/html/index.html # replacing in two steps to make sure api urls from releases are properly replaced as well
|
|
||||||
sed -i "s/'\/api\/v1/'$VIKUNJA_API_URL/g" /usr/share/nginx/html/index.html
|
|
||||||
sed -i "s/\.SENTRY_ENABLED = false/\.SENTRY_ENABLED = $VIKUNJA_SENTRY_ENABLED/g" /usr/share/nginx/html/index.html
|
|
||||||
sed -i "s|\.SENTRY_DSN = '.*'|\.SENTRY_DSN = '$VIKUNJA_SENTRY_DSN'|g" /usr/share/nginx/html/index.html
|
|
||||||
|
|
||||||
sed -i "s/listen 80/listen $VIKUNJA_HTTP_PORT/g" /etc/nginx/nginx.conf
|
|
||||||
sed -i "s/listen 443/listen $VIKUNJA_HTTPS_PORT/g" /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
# Set the uid and gid of the nginx run user
|
|
||||||
usermod --non-unique --uid ${PUID} nginx
|
|
||||||
groupmod --non-unique --gid ${PGID} nginx
|
|
||||||
|
|
||||||
nginx -g "daemon off;"
|
|
23
src/App.vue
23
src/App.vue
|
@ -8,9 +8,13 @@
|
||||||
<no-auth-wrapper v-else>
|
<no-auth-wrapper v-else>
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</no-auth-wrapper>
|
</no-auth-wrapper>
|
||||||
<Notification/>
|
|
||||||
|
|
||||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<UpdateNotification/>
|
||||||
|
<Notification/>
|
||||||
|
</Teleport>
|
||||||
</ready>
|
</ready>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -19,23 +23,26 @@ import {computed, watch} from 'vue'
|
||||||
import {useRoute, useRouter} from 'vue-router'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import isTouchDevice from 'is-touch-device'
|
import isTouchDevice from 'is-touch-device'
|
||||||
import {success} from '@/message'
|
|
||||||
|
|
||||||
import Notification from '@/components/misc/notification.vue'
|
import Notification from '@/components/misc/notification.vue'
|
||||||
import KeyboardShortcuts from './components/misc/keyboard-shortcuts/index.vue'
|
import UpdateNotification from '@/components/home/UpdateNotification.vue'
|
||||||
|
import KeyboardShortcuts from '@/components/misc/keyboard-shortcuts/index.vue'
|
||||||
|
|
||||||
import TheNavigation from '@/components/home/TheNavigation.vue'
|
import TheNavigation from '@/components/home/TheNavigation.vue'
|
||||||
import ContentAuth from './components/home/contentAuth.vue'
|
import ContentAuth from '@/components/home/contentAuth.vue'
|
||||||
import ContentLinkShare from './components/home/contentLinkShare.vue'
|
import ContentLinkShare from '@/components/home/contentLinkShare.vue'
|
||||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
||||||
import Ready from '@/components/misc/ready.vue'
|
import Ready from '@/components/misc/ready.vue'
|
||||||
|
|
||||||
import {setLanguage} from './i18n'
|
import {setLanguage} from '@/i18n'
|
||||||
import AccountDeleteService from '@/services/accountDelete'
|
import AccountDeleteService from '@/services/accountDelete'
|
||||||
|
import {success} from '@/message'
|
||||||
|
|
||||||
|
import {useAuthStore} from '@/stores/auth'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
|
||||||
import {useColorScheme} from '@/composables/useColorScheme'
|
import {useColorScheme} from '@/composables/useColorScheme'
|
||||||
import {useBodyClass} from '@/composables/useBodyClass'
|
import {useBodyClass} from '@/composables/useBodyClass'
|
||||||
import {useAuthStore} from './stores/auth'
|
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<update/>
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
@click="openQuickActions"
|
@click="openQuickActions"
|
||||||
class="trigger-button pr-0"
|
class="trigger-button pr-0"
|
||||||
|
@ -95,7 +94,6 @@ import {ref, computed, onMounted, nextTick} from 'vue'
|
||||||
|
|
||||||
import {RIGHTS as Rights} from '@/constants/rights'
|
import {RIGHTS as Rights} from '@/constants/rights'
|
||||||
|
|
||||||
import Update from '@/components/home/update.vue'
|
|
||||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
||||||
import Dropdown from '@/components/misc/dropdown.vue'
|
import Dropdown from '@/components/misc/dropdown.vue'
|
||||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="update-notification" v-if="updateAvailable">
|
<div class="update-notification" v-if="updateAvailable">
|
||||||
<p>{{ $t('update.available') }}</p>
|
<p class="update-notification__message">{{ $t('update.available') }}</p>
|
||||||
<x-button @click="refreshApp()" :shadow="false" class="has-no-text-wrap">
|
<x-button
|
||||||
|
@click="refreshApp()"
|
||||||
|
:shadow="false"
|
||||||
|
:wrap="false"
|
||||||
|
>
|
||||||
{{ $t('update.do') }}
|
{{ $t('update.do') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,15 +20,13 @@ const refreshing = ref(false)
|
||||||
|
|
||||||
document.addEventListener('swUpdated', showRefreshUI, {once: true})
|
document.addEventListener('swUpdated', showRefreshUI, {once: true})
|
||||||
|
|
||||||
if (navigator && navigator.serviceWorker) {
|
navigator?.serviceWorker?.addEventListener(
|
||||||
navigator.serviceWorker.addEventListener(
|
'controllerchange', () => {
|
||||||
'controllerchange', () => {
|
if (refreshing.value) return
|
||||||
if (refreshing.value) return
|
refreshing.value = true
|
||||||
refreshing.value = true
|
window.location.reload()
|
||||||
window.location.reload()
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function showRefreshUI(e: Event) {
|
function showRefreshUI(e: Event) {
|
||||||
console.log('recieved refresh event', e)
|
console.log('recieved refresh event', e)
|
||||||
|
@ -33,6 +35,7 @@ function showRefreshUI(e: Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshApp() {
|
function refreshApp() {
|
||||||
|
updateAvailable.value = false
|
||||||
if (!registration.value || !registration.value.waiting) {
|
if (!registration.value || !registration.value.waiting) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -43,39 +46,30 @@ function refreshApp() {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.update-notification {
|
.update-notification {
|
||||||
|
position: fixed;
|
||||||
|
// FIXME: We should prevent usage of z-index or
|
||||||
|
// at least define it centrally
|
||||||
|
// the highest z-index of a modal is .hint-modal with 4500
|
||||||
|
z-index: 5000;
|
||||||
|
bottom: 1rem;
|
||||||
|
inset-inline: 1rem;
|
||||||
|
max-width: max-content;
|
||||||
|
margin-inline: auto;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: $warning;
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
|
background: $warning;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
color: var(--grey-900);
|
color: var(--grey-900);
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
position: fixed;
|
|
||||||
bottom: 1rem;
|
|
||||||
width: 450px;
|
|
||||||
left: calc(50vw - 225px);
|
|
||||||
|
|
||||||
@media screen and (max-width: $tablet) {
|
|
||||||
position: fixed;
|
|
||||||
left: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
bottom: 1rem;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> * + * {
|
|
||||||
margin-left: .5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .update-notification {
|
.update-notification__message {
|
||||||
color: var(--grey-200);
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -224,9 +224,4 @@ labelStore.loadAllLabels()
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-touch .content-auth,
|
|
||||||
.content-auth.z-unset {
|
|
||||||
z-index: unset;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -8,17 +8,20 @@
|
||||||
'has-no-shadow': !shadow || variant === 'tertiary',
|
'has-no-shadow': !shadow || variant === 'tertiary',
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
|
:style="{
|
||||||
|
'--button-white-space': wrap ? 'break-spaces' : 'nowrap',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<template v-if="icon">
|
<template v-if="icon">
|
||||||
<icon
|
<icon
|
||||||
v-if="showIconOnly"
|
v-if="showIconOnly"
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
:style="{'color': iconColor !== '' ? iconColor : false}"
|
:style="{'color': iconColor !== '' ? iconColor : undefined}"
|
||||||
/>
|
/>
|
||||||
<span class="icon is-small" v-else>
|
<span class="icon is-small" v-else>
|
||||||
<icon
|
<icon
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
:style="{'color': iconColor !== '' ? iconColor : false}"
|
:style="{'color': iconColor !== '' ? iconColor : undefined}"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
@ -50,6 +53,7 @@ export interface ButtonProps extends BaseButtonProps {
|
||||||
iconColor?: string
|
iconColor?: string
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
shadow?: boolean
|
shadow?: boolean
|
||||||
|
wrap?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -58,6 +62,7 @@ const {
|
||||||
iconColor = '',
|
iconColor = '',
|
||||||
loading = false,
|
loading = false,
|
||||||
shadow = true,
|
shadow = true,
|
||||||
|
wrap = true,
|
||||||
} = defineProps<ButtonProps>()
|
} = defineProps<ButtonProps>()
|
||||||
|
|
||||||
const variantClass = computed(() => BUTTON_TYPES_MAP[variant])
|
const variantClass = computed(() => BUTTON_TYPES_MAP[variant])
|
||||||
|
@ -67,7 +72,7 @@ const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'und
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.button {
|
:where(.button) {
|
||||||
transition: all $transition;
|
transition: all $transition;
|
||||||
border: 0;
|
border: 0;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
@ -77,7 +82,7 @@ const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'und
|
||||||
min-height: $button-height;
|
min-height: $button-height;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
white-space: break-spaces;
|
white-space: var(--button-white-space);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
@ -99,7 +104,6 @@ const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'und
|
||||||
&.is-primary.is-outlined:hover {
|
&.is-primary.is-outlined:hover {
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-small {
|
.is-small {
|
||||||
|
|
14
src/helpers/getFullBaseUrl.ts
Normal file
14
src/helpers/getFullBaseUrl.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Get full BASE_URL
|
||||||
|
* - including path
|
||||||
|
* - will always end with a trailing slash
|
||||||
|
*/
|
||||||
|
export function getFullBaseUrl() {
|
||||||
|
// (1) The injected BASE_URL is declared from the `resolvedBase` that might miss a trailing slash...
|
||||||
|
// see: https://github.com/vitejs/vite/blob/b35fe883fdc699ac1450882562872095abe9959b/packages/vite/src/node/config.ts#LL614C25-L614C25
|
||||||
|
const rawBase = import.meta.env.BASE_URL
|
||||||
|
// (2) so we readd a slash like done here
|
||||||
|
// https://github.com/vitejs/vite/blob/b35fe883fdc699ac1450882562872095abe9959b/packages/vite/src/node/config.ts#L643
|
||||||
|
// See this comment: https://github.com/vitejs/vite/pull/10723#issuecomment-1303627478
|
||||||
|
return rawBase.endsWith('/') ? rawBase : rawBase + '/'
|
||||||
|
}
|
|
@ -233,7 +233,7 @@ export const getDateFromTextIn = (text: string, now: Date = new Date()) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDateFromWeekday = (text: string): dateFoundResult => {
|
const getDateFromWeekday = (text: string): dateFoundResult => {
|
||||||
const matcher = / (next )?(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|fri|saturday|sat|sunday|sun)($| )/g
|
const matcher = /(^| )(next )?(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|fri|saturday|sat|sunday|sun)($| )/g
|
||||||
const results: string[] | null = matcher.exec(text.toLowerCase()) // The i modifier does not seem to work.
|
const results: string[] | null = matcher.exec(text.toLowerCase()) // The i modifier does not seem to work.
|
||||||
if (results === null) {
|
if (results === null) {
|
||||||
return {
|
return {
|
||||||
|
@ -246,7 +246,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => {
|
||||||
const currentDay: number = date.getDay()
|
const currentDay: number = date.getDay()
|
||||||
let day = 0
|
let day = 0
|
||||||
|
|
||||||
switch (results[2]) {
|
switch (results[3]) {
|
||||||
case 'mon':
|
case 'mon':
|
||||||
case 'monday':
|
case 'monday':
|
||||||
day = 1
|
day = 1
|
||||||
|
|
|
@ -915,7 +915,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
"available": "There is an update for Vikunja available!",
|
"available": "There is an update available!",
|
||||||
"do": "Update Now"
|
"do": "Update Now"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -124,6 +124,18 @@ describe('Parse Task Text', () => {
|
||||||
expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
|
expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
|
||||||
expect(result?.date?.getDate()).toBe(nextMonday.getDate())
|
expect(result?.date?.getDate()).toBe(nextMonday.getDate())
|
||||||
})
|
})
|
||||||
|
it('should recognize next monday on the beginning of the sentence', () => {
|
||||||
|
const result = parseTaskText('next monday Lorem Ipsum')
|
||||||
|
|
||||||
|
const untilNextMonday = calculateDayInterval('nextMonday')
|
||||||
|
|
||||||
|
expect(result.text).toBe('Lorem Ipsum')
|
||||||
|
const nextMonday = new Date()
|
||||||
|
nextMonday.setDate(nextMonday.getDate() + untilNextMonday)
|
||||||
|
expect(result?.date?.getFullYear()).toBe(nextMonday.getFullYear())
|
||||||
|
expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
|
||||||
|
expect(result?.date?.getDate()).toBe(nextMonday.getDate())
|
||||||
|
})
|
||||||
it('should recognize next monday and ignore casing', () => {
|
it('should recognize next monday and ignore casing', () => {
|
||||||
const result = parseTaskText('Lorem Ipsum nExt Monday')
|
const result = parseTaskText('Lorem Ipsum nExt Monday')
|
||||||
|
|
||||||
|
@ -216,46 +228,7 @@ describe('Parse Task Text', () => {
|
||||||
expect(result?.date?.getDate()).toBe(date.getDate())
|
expect(result?.date?.getDate()).toBe(date.getDate())
|
||||||
})
|
})
|
||||||
|
|
||||||
const cases = {
|
|
||||||
'monday': 1,
|
|
||||||
'Monday': 1,
|
|
||||||
'mon': 1,
|
|
||||||
'Mon': 1,
|
|
||||||
'tuesday': 2,
|
|
||||||
'Tuesday': 2,
|
|
||||||
'tue': 2,
|
|
||||||
'Tue': 2,
|
|
||||||
'wednesday': 3,
|
|
||||||
'Wednesday': 3,
|
|
||||||
'wed': 3,
|
|
||||||
'Wed': 3,
|
|
||||||
'thursday': 4,
|
|
||||||
'Thursday': 4,
|
|
||||||
'thu': 4,
|
|
||||||
'Thu': 4,
|
|
||||||
'friday': 5,
|
|
||||||
'Friday': 5,
|
|
||||||
'fri': 5,
|
|
||||||
'Fri': 5,
|
|
||||||
'saturday': 6,
|
|
||||||
'Saturday': 6,
|
|
||||||
'sat': 6,
|
|
||||||
'Sat': 6,
|
|
||||||
'sunday': 7,
|
|
||||||
'Sunday': 7,
|
|
||||||
'sun': 7,
|
|
||||||
'Sun': 7,
|
|
||||||
} as Record<string, number>
|
|
||||||
for (const c in cases) {
|
|
||||||
it(`should recognize ${c} as weekday`, () => {
|
|
||||||
const result = parseTaskText(`Lorem Ipsum ${c}`)
|
|
||||||
|
|
||||||
expect(result.text).toBe('Lorem Ipsum')
|
|
||||||
const nextDate = new Date()
|
|
||||||
nextDate.setDate(nextDate.getDate() + ((cases[c] + 7 - nextDate.getDay()) % 7))
|
|
||||||
expect(`${result?.date?.getFullYear()}-${result?.date?.getMonth()}-${result?.date?.getDate()}`).toBe(`${nextDate.getFullYear()}-${nextDate.getMonth()}-${nextDate.getDate()}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
it('should recognize weekdays with time', () => {
|
it('should recognize weekdays with time', () => {
|
||||||
const result = parseTaskText('Lorem Ipsum thu at 14:00')
|
const result = parseTaskText('Lorem Ipsum thu at 14:00')
|
||||||
|
|
||||||
|
@ -369,20 +342,34 @@ describe('Parse Task Text', () => {
|
||||||
describe('Parse weekdays', () => {
|
describe('Parse weekdays', () => {
|
||||||
|
|
||||||
const days = {
|
const days = {
|
||||||
'mon': 1,
|
|
||||||
'monday': 1,
|
'monday': 1,
|
||||||
'tue': 2,
|
'Monday': 1,
|
||||||
|
'mon': 1,
|
||||||
|
'Mon': 1,
|
||||||
'tuesday': 2,
|
'tuesday': 2,
|
||||||
'wed': 3,
|
'Tuesday': 2,
|
||||||
|
'tue': 2,
|
||||||
|
'Tue': 2,
|
||||||
'wednesday': 3,
|
'wednesday': 3,
|
||||||
'thu': 4,
|
'Wednesday': 3,
|
||||||
|
'wed': 3,
|
||||||
|
'Wed': 3,
|
||||||
'thursday': 4,
|
'thursday': 4,
|
||||||
'fri': 5,
|
'Thursday': 4,
|
||||||
|
'thu': 4,
|
||||||
|
'Thu': 4,
|
||||||
'friday': 5,
|
'friday': 5,
|
||||||
'sat': 6,
|
'Friday': 5,
|
||||||
|
'fri': 5,
|
||||||
|
'Fri': 5,
|
||||||
'saturday': 6,
|
'saturday': 6,
|
||||||
'sun': 7,
|
'Saturday': 6,
|
||||||
|
'sat': 6,
|
||||||
|
'Sat': 6,
|
||||||
'sunday': 7,
|
'sunday': 7,
|
||||||
|
'Sunday': 7,
|
||||||
|
'sun': 7,
|
||||||
|
'Sun': 7,
|
||||||
} as Record<string, number>
|
} as Record<string, number>
|
||||||
|
|
||||||
const prefix = [
|
const prefix = [
|
||||||
|
@ -399,6 +386,18 @@ describe('Parse Task Text', () => {
|
||||||
const distance = (days[d] + 7 - next.getDay()) % 7
|
const distance = (days[d] + 7 - next.getDay()) % 7
|
||||||
next.setDate(next.getDate() + distance)
|
next.setDate(next.getDate() + distance)
|
||||||
|
|
||||||
|
expect(result.text).toBe('Lorem Ipsum')
|
||||||
|
expect(result?.date?.getFullYear()).toBe(next.getFullYear())
|
||||||
|
expect(result?.date?.getMonth()).toBe(next.getMonth())
|
||||||
|
expect(result?.date?.getDate()).toBe(next.getDate())
|
||||||
|
})
|
||||||
|
it(`should recognize ${p}${d} at the beginning of the text`, () => {
|
||||||
|
const result = parseTaskText(`${p}${d} Lorem Ipsum`)
|
||||||
|
|
||||||
|
const next = new Date()
|
||||||
|
const distance = (days[d] + 7 - next.getDay()) % 7
|
||||||
|
next.setDate(next.getDate() + distance)
|
||||||
|
|
||||||
expect(result.text).toBe('Lorem Ipsum')
|
expect(result.text).toBe('Lorem Ipsum')
|
||||||
expect(result?.date?.getFullYear()).toBe(next.getFullYear())
|
expect(result?.date?.getFullYear()).toBe(next.getFullYear())
|
||||||
expect(result?.date?.getMonth()).toBe(next.getMonth())
|
expect(result?.date?.getMonth()).toBe(next.getMonth())
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
import {register} from 'register-service-worker'
|
import {register} from 'register-service-worker'
|
||||||
|
|
||||||
|
import {getFullBaseUrl} from './helpers/getFullBaseUrl'
|
||||||
|
|
||||||
if (import.meta.env.PROD) {
|
if (import.meta.env.PROD) {
|
||||||
register('/sw.js', {
|
register(getFullBaseUrl() + 'sw.js', {
|
||||||
ready() {
|
ready() {
|
||||||
console.log('App is being served from cache by a service worker.')
|
console.log('App is being served from cache by a service worker.')
|
||||||
},
|
},
|
||||||
|
|
|
@ -81,7 +81,7 @@ const EditTeamComponent = () => import('@/views/teams/EditTeam.vue')
|
||||||
const NewTeamComponent = () => import('@/views/teams/NewTeam.vue')
|
const NewTeamComponent = () => import('@/views/teams/NewTeam.vue')
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
// If the user is using their forward/backward keys to navigate, we want to restore the scroll view
|
// If the user is using their forward/backward keys to navigate, we want to restore the scroll view
|
||||||
if (savedPosition) {
|
if (savedPosition) {
|
||||||
|
|
|
@ -124,10 +124,10 @@ export const useListStore = defineStore('list', () => {
|
||||||
...list,
|
...list,
|
||||||
namespaceId: FavoriteListsNamespace,
|
namespaceId: FavoriteListsNamespace,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespaceStore.removeListFromNamespaceById(newList)
|
||||||
if (list.isFavorite) {
|
if (list.isFavorite) {
|
||||||
namespaceStore.addListToNamespace(newList)
|
namespaceStore.addListToNamespace(newList)
|
||||||
} else {
|
|
||||||
namespaceStore.removeListFromNamespaceById(newList)
|
|
||||||
}
|
}
|
||||||
namespaceStore.loadNamespacesIfFavoritesDontExist()
|
namespaceStore.loadNamespacesIfFavoritesDontExist()
|
||||||
namespaceStore.removeFavoritesNamespaceIfEmpty()
|
namespaceStore.removeFavoritesNamespaceIfEmpty()
|
||||||
|
|
|
@ -2,8 +2,4 @@
|
||||||
@media print {
|
@media print {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-no-text-wrap {
|
|
||||||
white-space: nowrap !important;
|
|
||||||
}
|
|
12
src/sw.ts
12
src/sw.ts
|
@ -1,10 +1,16 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
/* eslint-disable no-undef */
|
/* eslint-disable no-undef */
|
||||||
|
|
||||||
|
import {getFullBaseUrl} from './helpers/getFullBaseUrl'
|
||||||
|
|
||||||
|
declare let self: ServiceWorkerGlobalScope
|
||||||
|
|
||||||
|
const fullBaseUrl = getFullBaseUrl()
|
||||||
const workboxVersion = 'v6.5.4'
|
const workboxVersion = 'v6.5.4'
|
||||||
importScripts( `/workbox-${workboxVersion}/workbox-sw.js`)
|
|
||||||
|
importScripts(`${fullBaseUrl}workbox-${workboxVersion}/workbox-sw.js`)
|
||||||
workbox.setConfig({
|
workbox.setConfig({
|
||||||
modulePathPrefix: `/workbox-${workboxVersion}`,
|
modulePathPrefix: `${fullBaseUrl}workbox-${workboxVersion}`,
|
||||||
debug: Boolean(import.meta.env.VITE_WORKBOX_DEBUG),
|
debug: Boolean(import.meta.env.VITE_WORKBOX_DEBUG),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -47,7 +53,7 @@ self.addEventListener('notificationclick', function (event) {
|
||||||
|
|
||||||
switch (event.action) {
|
switch (event.action) {
|
||||||
case 'show-task':
|
case 'show-task':
|
||||||
clients.openWindow(`/tasks/${taskId}`)
|
clients.openWindow(`${fullBaseUrl}tasks/${taskId}`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext", "DOM", "WebWorker"],
|
||||||
|
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|
281
vite.config.ts
281
vite.config.ts
|
@ -1,14 +1,14 @@
|
||||||
/// <reference types="vitest" />
|
/// <reference types="vitest" />
|
||||||
import {defineConfig, type PluginOption} from 'vite'
|
import {defineConfig, type PluginOption, loadEnv} from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import legacyFn from '@vitejs/plugin-legacy'
|
import legacyFn from '@vitejs/plugin-legacy'
|
||||||
import { URL, fileURLToPath } from 'node:url'
|
import {URL, fileURLToPath} from 'node:url'
|
||||||
import { dirname, resolve } from 'node:path'
|
import {dirname, resolve} from 'node:path'
|
||||||
|
|
||||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
||||||
import {VitePWA} from 'vite-plugin-pwa'
|
import {VitePWA} from 'vite-plugin-pwa'
|
||||||
import VitePluginInjectPreload from 'vite-plugin-inject-preload'
|
import VitePluginInjectPreload from 'vite-plugin-inject-preload'
|
||||||
import {visualizer} from 'rollup-plugin-visualizer'
|
import {visualizer} from 'rollup-plugin-visualizer'
|
||||||
import svgLoader from 'vite-svg-loader'
|
import svgLoader from 'vite-svg-loader'
|
||||||
import postcssPresetEnv from 'postcss-preset-env'
|
import postcssPresetEnv from 'postcss-preset-env'
|
||||||
import postcssEasings from 'postcss-easings'
|
import postcssEasings from 'postcss-easings'
|
||||||
|
@ -41,7 +41,7 @@ function createFontMatcher(fontNames: string[]) {
|
||||||
// The `match` option for the files of VitePluginInjectPreload
|
// The `match` option for the files of VitePluginInjectPreload
|
||||||
// matches the _output_ files.
|
// matches the _output_ files.
|
||||||
// Since we only want to mach variable fonts, we exploit here the fact
|
// Since we only want to mach variable fonts, we exploit here the fact
|
||||||
// that we added the `wght` term to indicate the variable weiht axis.
|
// that we added the `wght` term to indicate the variable weight axis.
|
||||||
// The format is something like:
|
// The format is something like:
|
||||||
// `/assets/OpenSans-Italic_wght__c9a8fe68-5f21f1e7.woff2`
|
// `/assets/OpenSans-Italic_wght__c9a8fe68-5f21f1e7.woff2`
|
||||||
// see: https://regex101.com/r/UgUWr1/1
|
// see: https://regex101.com/r/UgUWr1/1
|
||||||
|
@ -49,146 +49,143 @@ function createFontMatcher(fontNames: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({mode}) => {
|
||||||
// https://vitest.dev/config/
|
// Load env file based on `mode` in the current working directory.
|
||||||
test: {
|
// Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
|
||||||
environment: 'happy-dom',
|
// https://vitejs.dev/config/#environment-variables
|
||||||
},
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
css: {
|
|
||||||
preprocessorOptions: {
|
return {
|
||||||
scss: {
|
base: env.VIKUNJA_FRONTEND_BASE,
|
||||||
additionalData: PREFIXED_SCSS_STYLES,
|
// https://vitest.dev/config/
|
||||||
charset: false, // fixes "@charset" must be the first rule in the file" warnings
|
test: {
|
||||||
|
environment: 'happy-dom',
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: PREFIXED_SCSS_STYLES,
|
||||||
|
charset: false, // fixes "@charset" must be the first rule in the file" warnings
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
postcss: {
|
||||||
postcss: {
|
plugins: [
|
||||||
plugins: [
|
postcssEasings(),
|
||||||
postcssEasings(),
|
postcssEasingGradients(),
|
||||||
postcssEasingGradients(),
|
postcssPresetEnv(),
|
||||||
postcssPresetEnv({
|
|
||||||
// These plugins are enabled by default but require
|
|
||||||
// a polyfill that we don't include
|
|
||||||
// see also './src/polyfills.ts'
|
|
||||||
features: {
|
|
||||||
'blank-pseudo-class': false,
|
|
||||||
'focus-visible-pseudo-class': false,
|
|
||||||
'has-pseudo-class': false,
|
|
||||||
'prefers-color-scheme-query': false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
vue({
|
|
||||||
reactivityTransform: true,
|
|
||||||
}),
|
|
||||||
legacy,
|
|
||||||
svgLoader({
|
|
||||||
// Since the svgs are already manually optimized via https://jakearchibald.github.io/svgomg/
|
|
||||||
// we don't need to optimize them again.
|
|
||||||
svgo: false,
|
|
||||||
}),
|
|
||||||
VueI18nPlugin({
|
|
||||||
// TODO: only install needed stuff
|
|
||||||
// Whether to install the full set of APIs, components, etc. provided by Vue I18n.
|
|
||||||
// By default, all of them will be installed.
|
|
||||||
fullInstall: true,
|
|
||||||
include: resolve(dirname(pathSrc), './src/i18n/lang/**'),
|
|
||||||
}),
|
|
||||||
// https://github.com/Applelo/vite-plugin-inject-preload
|
|
||||||
VitePluginInjectPreload({
|
|
||||||
files: [{
|
|
||||||
match: createFontMatcher(['Quicksand', 'OpenSans', 'OpenSans-Italic']),
|
|
||||||
attributes: {crossorigin: 'anonymous'},
|
|
||||||
}],
|
|
||||||
injectTo: 'custom',
|
|
||||||
}),
|
|
||||||
VitePWA({
|
|
||||||
srcDir: 'src',
|
|
||||||
filename: 'sw.ts',
|
|
||||||
base: '/',
|
|
||||||
strategies: 'injectManifest',
|
|
||||||
injectRegister: false,
|
|
||||||
manifest: {
|
|
||||||
name: 'Vikunja',
|
|
||||||
short_name: 'Vikunja',
|
|
||||||
theme_color: '#1973ff',
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: './images/icons/android-chrome-192x192.png',
|
|
||||||
sizes: '192x192',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: './images/icons/android-chrome-512x512.png',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: './images/icons/icon-maskable.png',
|
|
||||||
sizes: '1024x1024',
|
|
||||||
type: 'image/png',
|
|
||||||
purpose: 'maskable',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
start_url: '.',
|
|
||||||
display: 'standalone',
|
|
||||||
background_color: '#000000',
|
|
||||||
shortcuts: [
|
|
||||||
{
|
|
||||||
name: 'Overview',
|
|
||||||
url: '/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Namespaces And Lists Overview',
|
|
||||||
short_name: 'Namespaces & Lists',
|
|
||||||
url: '/namespaces',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Tasks Next Week',
|
|
||||||
short_name: 'Next Week',
|
|
||||||
url: '/tasks/by/week',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Tasks Next Month',
|
|
||||||
short_name: 'Next Month',
|
|
||||||
url: '/tasks/by/month',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Teams Overview',
|
|
||||||
short_name: 'Teams',
|
|
||||||
url: '/teams',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
],
|
plugins: [
|
||||||
resolve: {
|
vue({
|
||||||
alias: [
|
reactivityTransform: true,
|
||||||
{
|
}),
|
||||||
find: '@',
|
legacy,
|
||||||
replacement: pathSrc,
|
svgLoader({
|
||||||
},
|
// Since the svgs are already manually optimized via https://jakearchibald.github.io/svgomg/
|
||||||
|
// we don't need to optimize them again.
|
||||||
|
svgo: false,
|
||||||
|
}),
|
||||||
|
VueI18nPlugin({
|
||||||
|
// TODO: only install needed stuff
|
||||||
|
// Whether to install the full set of APIs, components, etc. provided by Vue I18n.
|
||||||
|
// By default, all of them will be installed.
|
||||||
|
fullInstall: true,
|
||||||
|
include: resolve(dirname(pathSrc), './src/i18n/lang/**'),
|
||||||
|
}),
|
||||||
|
// https://github.com/Applelo/vite-plugin-inject-preload
|
||||||
|
VitePluginInjectPreload({
|
||||||
|
files: [{
|
||||||
|
match: createFontMatcher(['Quicksand', 'OpenSans', 'OpenSans-Italic']),
|
||||||
|
attributes: {crossorigin: 'anonymous'},
|
||||||
|
}],
|
||||||
|
injectTo: 'custom',
|
||||||
|
}),
|
||||||
|
VitePWA({
|
||||||
|
srcDir: 'src',
|
||||||
|
filename: 'sw.ts',
|
||||||
|
strategies: 'injectManifest',
|
||||||
|
injectRegister: false,
|
||||||
|
manifest: {
|
||||||
|
name: 'Vikunja',
|
||||||
|
short_name: 'Vikunja',
|
||||||
|
theme_color: '#1973ff',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: './images/icons/android-chrome-192x192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: './images/icons/android-chrome-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: './images/icons/icon-maskable.png',
|
||||||
|
sizes: '1024x1024',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'maskable',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
start_url: '.',
|
||||||
|
display: 'standalone',
|
||||||
|
background_color: '#000000',
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
name: 'Overview',
|
||||||
|
url: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Namespaces And Lists Overview',
|
||||||
|
short_name: 'Namespaces & Lists',
|
||||||
|
url: '/namespaces',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tasks Next Week',
|
||||||
|
short_name: 'Next Week',
|
||||||
|
url: '/tasks/by/week',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tasks Next Month',
|
||||||
|
short_name: 'Next Month',
|
||||||
|
url: '/tasks/by/month',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Teams Overview',
|
||||||
|
short_name: 'Teams',
|
||||||
|
url: '/teams',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
resolve: {
|
||||||
},
|
alias: [
|
||||||
server: {
|
{
|
||||||
host: '127.0.0.1', // see: https://github.com/vitejs/vite/pull/8543
|
find: '@',
|
||||||
port: 4173,
|
replacement: pathSrc,
|
||||||
strictPort: true,
|
},
|
||||||
},
|
|
||||||
build: {
|
|
||||||
target: 'esnext',
|
|
||||||
rollupOptions: {
|
|
||||||
plugins: [
|
|
||||||
visualizer({
|
|
||||||
filename: 'stats.html',
|
|
||||||
gzipSize: true,
|
|
||||||
// template: 'sunburst',
|
|
||||||
// brotliSize: true,
|
|
||||||
}) as PluginOption,
|
|
||||||
],
|
],
|
||||||
|
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||||
},
|
},
|
||||||
},
|
server: {
|
||||||
|
host: '127.0.0.1', // see: https://github.com/vitejs/vite/pull/8543
|
||||||
|
port: 4173,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
rollupOptions: {
|
||||||
|
plugins: [
|
||||||
|
visualizer({
|
||||||
|
filename: 'stats.html',
|
||||||
|
gzipSize: true,
|
||||||
|
// template: 'sunburst',
|
||||||
|
// brotliSize: true,
|
||||||
|
}) as PluginOption,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue
Block a user