mirror of
https://github.com/thiloho/archtika.git
synced 2025-11-22 10:51:36 +01:00
Merge pull request #16 from archtika/devel
Refactor web code, database migrations and set security headers
This commit is contained in:
8
flake.lock
generated
8
flake.lock
generated
@@ -2,16 +2,16 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1721497942,
|
"lastModified": 1726463316,
|
||||||
"narHash": "sha256-EDPL9qJfklXoowl3nEBmjDIqcvXKUZInt5n6CCc1Hn4=",
|
"narHash": "sha256-gI9kkaH0ZjakJOKrdjaI/VbaMEo9qBbSUl93DnU7f4c=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d43f0636fc9492e83be8bbb41f9595d7a87106b8",
|
"rev": "99dc8785f6a0adac95f5e2ab05cc2e1bf666d172",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixpkgs-unstable",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
acmeEmail = "thilo.hohlt@tutanota.com";
|
acmeEmail = "thilo.hohlt@tutanota.com";
|
||||||
dnsProvider = "porkbun";
|
dnsProvider = "porkbun";
|
||||||
dnsEnvironmentFile = /var/lib/porkbun.env;
|
dnsEnvironmentFile = /var/lib/porkbun.env;
|
||||||
|
disableRegistration = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ in
|
|||||||
default = null;
|
default = null;
|
||||||
description = "API secrets for the DNS-01 challenge (required for wildcard domains).";
|
description = "API secrets for the DNS-01 challenge (required for wildcard domains).";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
disableRegistration = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "By default any user can create an account. That behavior can be disabled by using this option.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
@@ -108,7 +114,7 @@ in
|
|||||||
|
|
||||||
${pkgs.dbmate}/bin/dbmate --url postgres://postgres@localhost:5432/archtika?sslmode=disable --migrations-dir ${cfg.package}/rest-api/db/migrations up
|
${pkgs.dbmate}/bin/dbmate --url postgres://postgres@localhost:5432/archtika?sslmode=disable --migrations-dir ${cfg.package}/rest-api/db/migrations up
|
||||||
|
|
||||||
PGRST_ADMIN_SERVER_PORT=${toString cfg.apiAdminPort} PGRST_SERVER_PORT=${toString cfg.apiPort} PGRST_DB_SCHEMAS="api" PGRST_DB_ANON_ROLE="anon" PGRST_OPENAPI_MODE="ignore-privileges" PGRST_DB_URI="postgres://authenticator@localhost:5432/${cfg.databaseName}" PGRST_JWT_SECRET="$JWT_SECRET" ${pkgs.postgrest}/bin/postgrest
|
PGRST_SERVER_CORS_ALLOWED_ORIGINS="https://${cfg.domain}" PGRST_ADMIN_SERVER_PORT=${toString cfg.apiAdminPort} PGRST_SERVER_PORT=${toString cfg.apiPort} PGRST_DB_SCHEMAS="api" PGRST_DB_ANON_ROLE="anon" PGRST_OPENAPI_MODE="ignore-privileges" PGRST_DB_URI="postgres://authenticator@localhost:5432/${cfg.databaseName}" PGRST_JWT_SECRET="$JWT_SECRET" ${pkgs.postgrest}/bin/postgrest
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,7 +131,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
script = ''
|
script = ''
|
||||||
BODY_SIZE_LIMIT=Infinity ORIGIN=https://${cfg.domain} PORT=${toString cfg.webAppPort} ${pkgs.nodejs_22}/bin/node ${cfg.package}/web-app
|
REGISTRATION_IS_DISABLED=${toString cfg.disableRegistration} BODY_SIZE_LIMIT=10M ORIGIN=https://${cfg.domain} PORT=${toString cfg.webAppPort} ${pkgs.nodejs_22}/bin/node ${cfg.package}/web-app
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,6 +154,20 @@ in
|
|||||||
enable = true;
|
enable = true;
|
||||||
recommendedProxySettings = true;
|
recommendedProxySettings = true;
|
||||||
recommendedTlsSettings = true;
|
recommendedTlsSettings = true;
|
||||||
|
recommendedZstdSettings = true;
|
||||||
|
recommendedOptimisation = true;
|
||||||
|
|
||||||
|
appendHttpConfig = ''
|
||||||
|
limit_req_zone $binary_remote_addr zone=requestLimit:10m rate=5r/s;
|
||||||
|
limit_req_status 429;
|
||||||
|
limit_req zone=requestLimit burst=20 nodelay;
|
||||||
|
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "accelerometer=(),autoplay=(),camera=(),cross-origin-isolated=(),display-capture=(),encrypted-media=(),fullscreen=(self),geolocation=(),gyroscope=(),keyboard-map=(),magnetometer=(),microphone=(),midi=(),payment=(),picture-in-picture=(self),publickey-credentials-get=(),screen-wake-lock=(),sync-xhr=(self),usb=(),xr-spatial-tracking=(),clipboard-read=(self),clipboard-write=(self),gamepad=(),hid=(),idle-detection=(),interest-cohort=(),serial=(),unload=()" always;
|
||||||
|
'';
|
||||||
|
|
||||||
virtualHosts = {
|
virtualHosts = {
|
||||||
"${cfg.domain}" = {
|
"${cfg.domain}" = {
|
||||||
@@ -166,13 +186,16 @@ in
|
|||||||
proxyPass = "http://localhost:${toString cfg.apiPort}/";
|
proxyPass = "http://localhost:${toString cfg.apiPort}/";
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
default_type application/json;
|
default_type application/json;
|
||||||
proxy_set_header Connection "";
|
'';
|
||||||
proxy_http_version 1.1;
|
};
|
||||||
|
"/api/rpc/register" = mkIf cfg.disableRegistration {
|
||||||
|
extraConfig = ''
|
||||||
|
deny all;
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"~^(?<subdomain>.+)\\.${lib.strings.escapeRegex cfg.domain}$" = {
|
"~^(?<subdomain>.+)\\.${cfg.domain}$" = {
|
||||||
useACMEHost = cfg.domain;
|
useACMEHost = cfg.domain;
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
locations = {
|
locations = {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ CREATE TABLE internal.user (
|
|||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
|
||||||
username VARCHAR(16) UNIQUE NOT NULL CHECK (LENGTH(username) >= 3),
|
username VARCHAR(16) UNIQUE NOT NULL CHECK (LENGTH(username) >= 3),
|
||||||
password_hash CHAR(60) NOT NULL,
|
password_hash CHAR(60) NOT NULL,
|
||||||
role NAME NOT NULL DEFAULT 'authenticated_user'
|
role NAME NOT NULL DEFAULT 'authenticated_user',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE internal.website (
|
CREATE TABLE internal.website (
|
||||||
@@ -33,8 +34,8 @@ CREATE TABLE internal.website (
|
|||||||
user_id UUID REFERENCES internal.user (id) ON DELETE CASCADE NOT NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
|
user_id UUID REFERENCES internal.user (id) ON DELETE CASCADE NOT NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
|
||||||
content_type VARCHAR(10) CHECK (content_type IN ('Blog', 'Docs')) NOT NULL,
|
content_type VARCHAR(10) CHECK (content_type IN ('Blog', 'Docs')) NOT NULL,
|
||||||
title VARCHAR(50) NOT NULL CHECK (TRIM(title) != ''),
|
title VARCHAR(50) NOT NULL CHECK (TRIM(title) != ''),
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
|
||||||
is_published BOOLEAN NOT NULL DEFAULT FALSE,
|
is_published BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||||
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||||
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL,
|
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL,
|
||||||
title_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', title)) STORED
|
title_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', title)) STORED
|
||||||
@@ -52,8 +53,10 @@ CREATE TABLE internal.media (
|
|||||||
|
|
||||||
CREATE TABLE internal.settings (
|
CREATE TABLE internal.settings (
|
||||||
website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
|
website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
|
||||||
accent_color_light_theme CHAR(7) CHECK (accent_color_light_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#a5d8ff',
|
accent_color_dark_theme CHAR(7) CHECK (accent_color_light_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#a5d8ff',
|
||||||
accent_color_dark_theme CHAR(7) CHECK (accent_color_dark_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#114678',
|
accent_color_light_theme CHAR(7) CHECK (accent_color_dark_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#114678',
|
||||||
|
background_color_dark_theme CHAR(7) CHECK (accent_color_light_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#262626',
|
||||||
|
background_color_light_theme CHAR(7) CHECK (accent_color_dark_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#ffffff',
|
||||||
favicon_image UUID REFERENCES internal.media (id) ON DELETE SET NULL,
|
favicon_image UUID REFERENCES internal.media (id) ON DELETE SET NULL,
|
||||||
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||||
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
|
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
|
||||||
@@ -82,6 +85,7 @@ CREATE TABLE internal.docs_category (
|
|||||||
user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
|
user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
|
||||||
category_name VARCHAR(50) NOT NULL CHECK (TRIM(category_name) != ''),
|
category_name VARCHAR(50) NOT NULL CHECK (TRIM(category_name) != ''),
|
||||||
category_weight INTEGER CHECK (category_weight >= 0) NOT NULL,
|
category_weight INTEGER CHECK (category_weight >= 0) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||||
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||||
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL,
|
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL,
|
||||||
UNIQUE (website_id, category_name),
|
UNIQUE (website_id, category_name),
|
||||||
@@ -117,6 +121,7 @@ CREATE TABLE internal.footer (
|
|||||||
CREATE TABLE internal.legal_information (
|
CREATE TABLE internal.legal_information (
|
||||||
website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
|
website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
|
||||||
main_content TEXT NOT NULL CHECK (TRIM(main_content) != ''),
|
main_content TEXT NOT NULL CHECK (TRIM(main_content) != ''),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||||
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||||
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
|
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -127,39 +127,39 @@ GRANT SELECT ON api.account TO authenticated_user;
|
|||||||
|
|
||||||
GRANT SELECT ON api.user TO authenticated_user;
|
GRANT SELECT ON api.user TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, UPDATE, DELETE ON internal.website TO authenticated_user;
|
GRANT SELECT, UPDATE (title, is_published), DELETE ON internal.website TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
|
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, UPDATE ON internal.settings TO authenticated_user;
|
GRANT SELECT, UPDATE (accent_color_dark_theme, accent_color_light_theme, background_color_dark_theme, background_color_light_theme, favicon_image) ON internal.settings TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, UPDATE ON api.settings TO authenticated_user;
|
GRANT SELECT, UPDATE ON api.settings TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, UPDATE ON internal.header TO authenticated_user;
|
GRANT SELECT, UPDATE (logo_type, logo_text, logo_image) ON internal.header TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, UPDATE ON api.header TO authenticated_user;
|
GRANT SELECT, UPDATE ON api.header TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, UPDATE ON internal.home TO authenticated_user;
|
GRANT SELECT, UPDATE (main_content) ON internal.home TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, UPDATE ON api.home TO authenticated_user;
|
GRANT SELECT, UPDATE ON api.home TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.article TO authenticated_user;
|
GRANT SELECT, INSERT (website_id, title, meta_description, meta_author, cover_image, publication_date, main_content, category, article_weight), UPDATE (title, meta_description, meta_author, cover_image, publication_date, main_content, category, article_weight), DELETE ON internal.article TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
|
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.docs_category TO authenticated_user;
|
GRANT SELECT, INSERT (website_id, category_name, category_weight), UPDATE (category_name, category_weight), DELETE ON internal.docs_category TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.docs_category TO authenticated_user;
|
GRANT SELECT, INSERT, UPDATE, DELETE ON api.docs_category TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, UPDATE ON internal.footer TO authenticated_user;
|
GRANT SELECT, UPDATE (additional_text) ON internal.footer TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, UPDATE ON api.footer TO authenticated_user;
|
GRANT SELECT, UPDATE ON api.footer TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.legal_information TO authenticated_user;
|
GRANT SELECT, INSERT (website_id, main_content), UPDATE (website_id, main_content), DELETE ON internal.legal_information TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.legal_information TO authenticated_user;
|
GRANT SELECT, INSERT, UPDATE, DELETE ON api.legal_information TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.collab TO authenticated_user;
|
GRANT SELECT, INSERT (website_id, user_id, permission_level), UPDATE (permission_level), DELETE ON internal.collab TO authenticated_user;
|
||||||
|
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.collab TO authenticated_user;
|
GRANT SELECT, INSERT, UPDATE, DELETE ON api.collab TO authenticated_user;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ BEGIN
|
|||||||
RETURN COALESCE(NEW, OLD);
|
RETURN COALESCE(NEW, OLD);
|
||||||
END;
|
END;
|
||||||
$$
|
$$
|
||||||
LANGUAGE plpgsql;
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER;
|
||||||
|
|
||||||
CREATE TRIGGER update_website_last_modified
|
CREATE TRIGGER update_website_last_modified
|
||||||
BEFORE UPDATE ON internal.website
|
BEFORE UPDATE ON internal.website
|
||||||
@@ -68,7 +69,7 @@ CREATE TRIGGER update_footer_last_modified
|
|||||||
EXECUTE FUNCTION internal.update_last_modified ();
|
EXECUTE FUNCTION internal.update_last_modified ();
|
||||||
|
|
||||||
CREATE TRIGGER update_legal_information_last_modified
|
CREATE TRIGGER update_legal_information_last_modified
|
||||||
BEFORE INSERT OR DELETE ON internal.legal_information
|
BEFORE INSERT OR UPDATE OR DELETE ON internal.legal_information
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION internal.update_last_modified ();
|
EXECUTE FUNCTION internal.update_last_modified ();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ DECLARE
|
|||||||
_website_id UUID := (_headers ->> 'x-website-id')::UUID;
|
_website_id UUID := (_headers ->> 'x-website-id')::UUID;
|
||||||
_mimetype TEXT := _headers ->> 'x-mimetype';
|
_mimetype TEXT := _headers ->> 'x-mimetype';
|
||||||
_original_filename TEXT := _headers ->> 'x-original-filename';
|
_original_filename TEXT := _headers ->> 'x-original-filename';
|
||||||
_allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp'];
|
_allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp', 'image/avif', 'image/gif', 'image/svg+xml'];
|
||||||
_max_file_size INT := 5 * 1024 * 1024;
|
_max_file_size INT := 5 * 1024 * 1024;
|
||||||
_has_access BOOLEAN;
|
_has_access BOOLEAN;
|
||||||
BEGIN
|
BEGIN
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- migrate:up
|
||||||
|
CREATE TABLE internal.domain_prefix (
|
||||||
|
website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
|
||||||
|
prefix VARCHAR(16) UNIQUE NOT NULL CHECK (LENGTH(prefix) >= 3 AND prefix ~ '^[a-z]+(-[a-z]+)*$'),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||||
|
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||||
|
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE VIEW api.domain_prefix WITH ( security_invoker = ON
|
||||||
|
) AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
internal.domain_prefix;
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT (website_id, prefix), UPDATE (website_id, prefix), DELETE ON internal.domain_prefix TO authenticated_user;
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON api.domain_prefix TO authenticated_user;
|
||||||
|
|
||||||
|
ALTER TABLE internal.domain_prefix ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY view_domain_prefix ON internal.domain_prefix
|
||||||
|
FOR SELECT
|
||||||
|
USING (internal.user_has_website_access (website_id, 10));
|
||||||
|
|
||||||
|
CREATE POLICY update_domain_prefix ON internal.domain_prefix
|
||||||
|
FOR UPDATE
|
||||||
|
USING (internal.user_has_website_access (website_id, 30));
|
||||||
|
|
||||||
|
CREATE POLICY delete_domain_prefix ON internal.domain_prefix
|
||||||
|
FOR DELETE
|
||||||
|
USING (internal.user_has_website_access (website_id, 30));
|
||||||
|
|
||||||
|
CREATE POLICY insert_domain_prefix ON internal.domain_prefix
|
||||||
|
FOR INSERT
|
||||||
|
WITH CHECK (internal.user_has_website_access (website_id, 30));
|
||||||
|
|
||||||
|
CREATE TRIGGER update_domain_prefix_last_modified
|
||||||
|
BEFORE INSERT OR UPDATE OR DELETE ON internal.domain_prefix
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION internal.update_last_modified ();
|
||||||
|
|
||||||
|
CREATE TRIGGER domain_prefix_track_changes
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON internal.domain_prefix
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION internal.track_changes ();
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
DROP TRIGGER domain_prefix_track_changes ON internal.domain_prefix;
|
||||||
|
|
||||||
|
DROP TRIGGER update_domain_prefix_last_modified ON internal.domain_prefix;
|
||||||
|
|
||||||
|
DROP VIEW api.domain_prefix;
|
||||||
|
|
||||||
|
DROP TABLE internal.domain_prefix;
|
||||||
|
|
||||||
829
web-app/package-lock.json
generated
829
web-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,34 +14,34 @@
|
|||||||
"gents": "pg-to-ts generate -c postgres://postgres@localhost:15432/archtika -o src/lib/db-schema.ts -s internal"
|
"gents": "pg-to-ts generate -c postgres://postgres@localhost:15432/archtika -o src/lib/db-schema.ts -s internal"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.40.0",
|
"@playwright/test": "1.46.0",
|
||||||
"@sveltejs/adapter-auto": "3.2.4",
|
"@sveltejs/adapter-auto": "3.2.5",
|
||||||
"@sveltejs/adapter-node": "5.2.2",
|
"@sveltejs/adapter-node": "5.2.3",
|
||||||
"@sveltejs/kit": "2.5.22",
|
"@sveltejs/kit": "2.5.28",
|
||||||
"@sveltejs/vite-plugin-svelte": "3.1.1",
|
"@sveltejs/vite-plugin-svelte": "4.0.0-next.6",
|
||||||
"@types/eslint": "9.6.1",
|
"@types/eslint": "9.6.1",
|
||||||
"@types/eslint__js": "8.42.3",
|
"@types/eslint__js": "8.42.3",
|
||||||
"@types/eslint-config-prettier": "6.11.3",
|
"@types/eslint-config-prettier": "6.11.3",
|
||||||
"@types/node": "22.2.0",
|
"@types/node": "22.5.5",
|
||||||
"eslint": "9.10.0",
|
"eslint": "9.10.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-svelte": "2.43.0",
|
"eslint-plugin-svelte": "2.44.0",
|
||||||
"globals": "15.9.0",
|
"globals": "15.9.0",
|
||||||
"pg-to-ts": "4.1.1",
|
"pg-to-ts": "4.1.1",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"prettier-plugin-svelte": "3.2.6",
|
"prettier-plugin-svelte": "3.2.6",
|
||||||
"svelte": "5.0.0-next.220",
|
"svelte": "5.0.0-next.253",
|
||||||
"svelte-check": "3.8.5",
|
"svelte-check": "4.0.2",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.6.2",
|
||||||
"typescript-eslint": "8.4.0",
|
"typescript-eslint": "8.6.0",
|
||||||
"vite": "5.4.0"
|
"vite": "5.4.6"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-diff": "1.3.0",
|
"fast-diff": "1.3.0",
|
||||||
"highlight.js": "11.10.0",
|
"highlight.js": "11.10.0",
|
||||||
"isomorphic-dompurify": "2.14.0",
|
"isomorphic-dompurify": "2.15.0",
|
||||||
"marked": "14.0.0",
|
"marked": "14.1.2",
|
||||||
"marked-highlight": "2.1.4"
|
"marked-highlight": "2.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
import { redirect } from "@sveltejs/kit";
|
import { redirect } from "@sveltejs/kit";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||||
import type { User } from "$lib/db-schema";
|
|
||||||
|
|
||||||
export const handle = async ({ event, resolve }) => {
|
export const handle = async ({ event, resolve }) => {
|
||||||
if (!event.url.pathname.startsWith("/api/")) {
|
if (!event.url.pathname.startsWith("/api/")) {
|
||||||
const userData = await event.fetch(`${API_BASE_PREFIX}/account`, {
|
const userData = await apiRequest(event.fetch, `${API_BASE_PREFIX}/account`, "GET", {
|
||||||
method: "GET",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
Accept: "application/vnd.pgrst.object+json",
|
||||||
Authorization: `Bearer ${event.cookies.get("session_token")}`,
|
Authorization: `Bearer ${event.cookies.get("session_token")}`
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
},
|
||||||
}
|
returnData: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userData.ok && !["/login", "/register"].includes(event.url.pathname)) {
|
if (!userData.success && !["/login", "/register"].includes(event.url.pathname)) {
|
||||||
throw redirect(303, "/login");
|
throw redirect(303, "/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userData.ok) {
|
if (userData.success) {
|
||||||
if (["/login", "/register"].includes(event.url.pathname)) {
|
if (["/login", "/register"].includes(event.url.pathname)) {
|
||||||
throw redirect(303, "/");
|
throw redirect(303, "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user: User = await userData.json();
|
event.locals.user = userData.data;
|
||||||
|
|
||||||
event.locals.user = user;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleFetch = async ({ event, request, fetch }) => {
|
||||||
|
if (event.locals.user) {
|
||||||
|
request.headers.set("Authorization", `Bearer ${event.cookies.get("session_token")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(request);
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
.spinner {
|
.spinner {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: var(--bg-blurred);
|
||||||
z-index: 40;
|
z-index: 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +27,6 @@
|
|||||||
border: var(--border-primary);
|
border: var(--border-primary);
|
||||||
border-width: 0.125rem;
|
border-width: 0.125rem;
|
||||||
border-block-start-color: var(--color-accent);
|
border-block-start-color: var(--color-accent);
|
||||||
animation: spinner 0.6s linear infinite;
|
animation: spinner 500ms linear infinite;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
72
web-app/src/lib/components/MarkdownEditor.svelte
Normal file
72
web-app/src/lib/components/MarkdownEditor.svelte
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { deserialize, applyAction } from "$app/forms";
|
||||||
|
import { textareaScrollTop, previewContent } from "$lib/runes.svelte";
|
||||||
|
|
||||||
|
const {
|
||||||
|
apiPrefix,
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
content
|
||||||
|
}: { apiPrefix: string; label: string; name: string; content: string } = $props();
|
||||||
|
|
||||||
|
let mainContentTextarea: HTMLTextAreaElement;
|
||||||
|
|
||||||
|
const updateScrollPercentage = () => {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = mainContentTextarea;
|
||||||
|
textareaScrollTop.value = (scrollTop / (scrollHeight - clientHeight)) * 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImagePaste = async (event: ClipboardEvent) => {
|
||||||
|
const clipboardItems = Array.from(event.clipboardData?.items ?? []);
|
||||||
|
const file = clipboardItems.find((item) => item.type.startsWith("image/"));
|
||||||
|
|
||||||
|
if (!file) return null;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const fileObject = file.getAsFile();
|
||||||
|
|
||||||
|
if (!fileObject) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", fileObject);
|
||||||
|
|
||||||
|
const request = await fetch("?/pasteImage", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = deserialize(await request.clone().text());
|
||||||
|
applyAction(result);
|
||||||
|
|
||||||
|
const response = await request.json();
|
||||||
|
|
||||||
|
if (JSON.parse(response.data)[1]) {
|
||||||
|
const fileId = JSON.parse(response.data)[4];
|
||||||
|
const fileUrl = `${apiPrefix}/rpc/retrieve_file?id=${fileId}`;
|
||||||
|
|
||||||
|
const target = event.target as HTMLTextAreaElement;
|
||||||
|
const newContent =
|
||||||
|
target.value.slice(0, target.selectionStart) +
|
||||||
|
`` +
|
||||||
|
target.value.slice(target.selectionStart);
|
||||||
|
|
||||||
|
previewContent.value = newContent;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
{label}:
|
||||||
|
<textarea
|
||||||
|
{name}
|
||||||
|
rows="20"
|
||||||
|
bind:value={previewContent.value}
|
||||||
|
bind:this={mainContentTextarea}
|
||||||
|
onscroll={updateScrollPercentage}
|
||||||
|
onpaste={handleImagePaste}
|
||||||
|
required>{content}</textarea
|
||||||
|
>
|
||||||
|
</label>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: var(--bg-blurred);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__content {
|
.modal__content {
|
||||||
|
|||||||
@@ -2,30 +2,27 @@
|
|||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import { md } from "$lib/utils";
|
import { md } from "$lib/utils";
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
|
import { previewContent, textareaScrollTop } from "$lib/runes.svelte";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
contentType,
|
contentType,
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
fullPreview = false,
|
fullPreview = false
|
||||||
previewContent,
|
|
||||||
previewScrollTop = 0
|
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
contentType: string;
|
contentType: string;
|
||||||
title: string;
|
title: string;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
fullPreview?: boolean;
|
fullPreview?: boolean;
|
||||||
previewContent: string;
|
|
||||||
previewScrollTop?: number;
|
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let previewElement: HTMLDivElement;
|
let previewElement: HTMLDivElement;
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const scrollHeight = previewElement.scrollHeight - previewElement.clientHeight;
|
const scrollHeight = previewElement.scrollHeight - previewElement.clientHeight;
|
||||||
previewElement.scrollTop = (previewScrollTop / 100) * scrollHeight;
|
previewElement.scrollTop = (textareaScrollTop.value / 100) * scrollHeight;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -66,9 +63,12 @@
|
|||||||
|
|
||||||
<div class="preview" bind:this={previewElement}>
|
<div class="preview" bind:this={previewElement}>
|
||||||
{#if fullPreview}
|
{#if fullPreview}
|
||||||
<iframe src={previewContent} title="Preview"></iframe>
|
<iframe src={previewContent.value} title="Preview"></iframe>
|
||||||
{:else}
|
{:else}
|
||||||
{@html md(previewContent, Object.keys($page.params).length > 1 ? true : false)}
|
{@html md(
|
||||||
|
previewContent.value || "Write some markdown content to see a live preview here",
|
||||||
|
Object.keys($page.params).length > 1 ? true : false
|
||||||
|
)}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* AUTO-GENERATED FILE - DO NOT EDIT!
|
* AUTO-GENERATED FILE - DO NOT EDIT!
|
||||||
*
|
*
|
||||||
* This file was automatically generated by pg-to-ts v.4.1.1
|
* This file was automatically generated by pg-to-ts v.4.1.1
|
||||||
* $ pg-to-ts generate -c postgres://username:password@localhost:15432/archtika -t article -t change_log -t collab -t docs_category -t footer -t header -t home -t legal_information -t media -t settings -t user -t website -s internal
|
* $ pg-to-ts generate -c postgres://username:password@localhost:15432/archtika -t article -t change_log -t collab -t docs_category -t domain_prefix -t footer -t header -t home -t legal_information -t media -t settings -t user -t website -s internal
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -169,6 +169,7 @@ export interface DocsCategory {
|
|||||||
user_id: string | null;
|
user_id: string | null;
|
||||||
category_name: string;
|
category_name: string;
|
||||||
category_weight: number;
|
category_weight: number;
|
||||||
|
created_at: Date;
|
||||||
last_modified_at: Date;
|
last_modified_at: Date;
|
||||||
last_modified_by: string | null;
|
last_modified_by: string | null;
|
||||||
}
|
}
|
||||||
@@ -178,6 +179,7 @@ export interface DocsCategoryInput {
|
|||||||
user_id?: string | null;
|
user_id?: string | null;
|
||||||
category_name: string;
|
category_name: string;
|
||||||
category_weight: number;
|
category_weight: number;
|
||||||
|
created_at?: Date;
|
||||||
last_modified_at?: Date;
|
last_modified_at?: Date;
|
||||||
last_modified_by?: string | null;
|
last_modified_by?: string | null;
|
||||||
}
|
}
|
||||||
@@ -189,6 +191,7 @@ const docs_category = {
|
|||||||
"user_id",
|
"user_id",
|
||||||
"category_name",
|
"category_name",
|
||||||
"category_weight",
|
"category_weight",
|
||||||
|
"created_at",
|
||||||
"last_modified_at",
|
"last_modified_at",
|
||||||
"last_modified_by"
|
"last_modified_by"
|
||||||
],
|
],
|
||||||
@@ -203,6 +206,34 @@ const docs_category = {
|
|||||||
$input: null as unknown as DocsCategoryInput
|
$input: null as unknown as DocsCategoryInput
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// Table domain_prefix
|
||||||
|
export interface DomainPrefix {
|
||||||
|
website_id: string;
|
||||||
|
prefix: string;
|
||||||
|
created_at: Date;
|
||||||
|
last_modified_at: Date;
|
||||||
|
last_modified_by: string | null;
|
||||||
|
}
|
||||||
|
export interface DomainPrefixInput {
|
||||||
|
website_id: string;
|
||||||
|
prefix: string;
|
||||||
|
created_at?: Date;
|
||||||
|
last_modified_at?: Date;
|
||||||
|
last_modified_by?: string | null;
|
||||||
|
}
|
||||||
|
const domain_prefix = {
|
||||||
|
tableName: "domain_prefix",
|
||||||
|
columns: ["website_id", "prefix", "created_at", "last_modified_at", "last_modified_by"],
|
||||||
|
requiredForInsert: ["website_id", "prefix"],
|
||||||
|
primaryKey: "website_id",
|
||||||
|
foreignKeys: {
|
||||||
|
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||||
|
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
|
||||||
|
},
|
||||||
|
$type: null as unknown as DomainPrefix,
|
||||||
|
$input: null as unknown as DomainPrefixInput
|
||||||
|
} as const;
|
||||||
|
|
||||||
// Table footer
|
// Table footer
|
||||||
export interface Footer {
|
export interface Footer {
|
||||||
website_id: string;
|
website_id: string;
|
||||||
@@ -297,18 +328,20 @@ const home = {
|
|||||||
export interface LegalInformation {
|
export interface LegalInformation {
|
||||||
website_id: string;
|
website_id: string;
|
||||||
main_content: string;
|
main_content: string;
|
||||||
|
created_at: Date;
|
||||||
last_modified_at: Date;
|
last_modified_at: Date;
|
||||||
last_modified_by: string | null;
|
last_modified_by: string | null;
|
||||||
}
|
}
|
||||||
export interface LegalInformationInput {
|
export interface LegalInformationInput {
|
||||||
website_id: string;
|
website_id: string;
|
||||||
main_content: string;
|
main_content: string;
|
||||||
|
created_at?: Date;
|
||||||
last_modified_at?: Date;
|
last_modified_at?: Date;
|
||||||
last_modified_by?: string | null;
|
last_modified_by?: string | null;
|
||||||
}
|
}
|
||||||
const legal_information = {
|
const legal_information = {
|
||||||
tableName: "legal_information",
|
tableName: "legal_information",
|
||||||
columns: ["website_id", "main_content", "last_modified_at", "last_modified_by"],
|
columns: ["website_id", "main_content", "created_at", "last_modified_at", "last_modified_by"],
|
||||||
requiredForInsert: ["website_id", "main_content"],
|
requiredForInsert: ["website_id", "main_content"],
|
||||||
primaryKey: "website_id",
|
primaryKey: "website_id",
|
||||||
foreignKeys: {
|
foreignKeys: {
|
||||||
@@ -354,16 +387,20 @@ const media = {
|
|||||||
// Table settings
|
// Table settings
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
website_id: string;
|
website_id: string;
|
||||||
accent_color_light_theme: string;
|
|
||||||
accent_color_dark_theme: string;
|
accent_color_dark_theme: string;
|
||||||
|
accent_color_light_theme: string;
|
||||||
|
background_color_dark_theme: string;
|
||||||
|
background_color_light_theme: string;
|
||||||
favicon_image: string | null;
|
favicon_image: string | null;
|
||||||
last_modified_at: Date;
|
last_modified_at: Date;
|
||||||
last_modified_by: string | null;
|
last_modified_by: string | null;
|
||||||
}
|
}
|
||||||
export interface SettingsInput {
|
export interface SettingsInput {
|
||||||
website_id: string;
|
website_id: string;
|
||||||
accent_color_light_theme?: string;
|
|
||||||
accent_color_dark_theme?: string;
|
accent_color_dark_theme?: string;
|
||||||
|
accent_color_light_theme?: string;
|
||||||
|
background_color_dark_theme?: string;
|
||||||
|
background_color_light_theme?: string;
|
||||||
favicon_image?: string | null;
|
favicon_image?: string | null;
|
||||||
last_modified_at?: Date;
|
last_modified_at?: Date;
|
||||||
last_modified_by?: string | null;
|
last_modified_by?: string | null;
|
||||||
@@ -372,8 +409,10 @@ const settings = {
|
|||||||
tableName: "settings",
|
tableName: "settings",
|
||||||
columns: [
|
columns: [
|
||||||
"website_id",
|
"website_id",
|
||||||
"accent_color_light_theme",
|
|
||||||
"accent_color_dark_theme",
|
"accent_color_dark_theme",
|
||||||
|
"accent_color_light_theme",
|
||||||
|
"background_color_dark_theme",
|
||||||
|
"background_color_light_theme",
|
||||||
"favicon_image",
|
"favicon_image",
|
||||||
"last_modified_at",
|
"last_modified_at",
|
||||||
"last_modified_by"
|
"last_modified_by"
|
||||||
@@ -395,16 +434,18 @@ export interface User {
|
|||||||
username: string;
|
username: string;
|
||||||
password_hash: string;
|
password_hash: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
created_at: Date;
|
||||||
}
|
}
|
||||||
export interface UserInput {
|
export interface UserInput {
|
||||||
id?: string;
|
id?: string;
|
||||||
username: string;
|
username: string;
|
||||||
password_hash: string;
|
password_hash: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
|
created_at?: Date;
|
||||||
}
|
}
|
||||||
const user = {
|
const user = {
|
||||||
tableName: "user",
|
tableName: "user",
|
||||||
columns: ["id", "username", "password_hash", "role"],
|
columns: ["id", "username", "password_hash", "role", "created_at"],
|
||||||
requiredForInsert: ["username", "password_hash"],
|
requiredForInsert: ["username", "password_hash"],
|
||||||
primaryKey: "id",
|
primaryKey: "id",
|
||||||
foreignKeys: {},
|
foreignKeys: {},
|
||||||
@@ -418,8 +459,8 @@ export interface Website {
|
|||||||
user_id: string;
|
user_id: string;
|
||||||
content_type: string;
|
content_type: string;
|
||||||
title: string;
|
title: string;
|
||||||
created_at: Date;
|
|
||||||
is_published: boolean;
|
is_published: boolean;
|
||||||
|
created_at: Date;
|
||||||
last_modified_at: Date;
|
last_modified_at: Date;
|
||||||
last_modified_by: string | null;
|
last_modified_by: string | null;
|
||||||
title_search: any | null;
|
title_search: any | null;
|
||||||
@@ -429,8 +470,8 @@ export interface WebsiteInput {
|
|||||||
user_id?: string;
|
user_id?: string;
|
||||||
content_type: string;
|
content_type: string;
|
||||||
title: string;
|
title: string;
|
||||||
created_at?: Date;
|
|
||||||
is_published?: boolean;
|
is_published?: boolean;
|
||||||
|
created_at?: Date;
|
||||||
last_modified_at?: Date;
|
last_modified_at?: Date;
|
||||||
last_modified_by?: string | null;
|
last_modified_by?: string | null;
|
||||||
title_search?: any | null;
|
title_search?: any | null;
|
||||||
@@ -442,8 +483,8 @@ const website = {
|
|||||||
"user_id",
|
"user_id",
|
||||||
"content_type",
|
"content_type",
|
||||||
"title",
|
"title",
|
||||||
"created_at",
|
|
||||||
"is_published",
|
"is_published",
|
||||||
|
"created_at",
|
||||||
"last_modified_at",
|
"last_modified_at",
|
||||||
"last_modified_by",
|
"last_modified_by",
|
||||||
"title_search"
|
"title_search"
|
||||||
@@ -475,6 +516,10 @@ export interface TableTypes {
|
|||||||
select: DocsCategory;
|
select: DocsCategory;
|
||||||
input: DocsCategoryInput;
|
input: DocsCategoryInput;
|
||||||
};
|
};
|
||||||
|
domain_prefix: {
|
||||||
|
select: DomainPrefix;
|
||||||
|
input: DomainPrefixInput;
|
||||||
|
};
|
||||||
footer: {
|
footer: {
|
||||||
select: Footer;
|
select: Footer;
|
||||||
input: FooterInput;
|
input: FooterInput;
|
||||||
@@ -514,6 +559,7 @@ export const tables = {
|
|||||||
change_log,
|
change_log,
|
||||||
collab,
|
collab,
|
||||||
docs_category,
|
docs_category,
|
||||||
|
domain_prefix,
|
||||||
footer,
|
footer,
|
||||||
header,
|
header,
|
||||||
home,
|
home,
|
||||||
|
|||||||
30
web-app/src/lib/runes.svelte.ts
Normal file
30
web-app/src/lib/runes.svelte.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
let sendingState = $state(false);
|
||||||
|
let previewContentState = $state("");
|
||||||
|
let textareaScrollTopState = $state(0);
|
||||||
|
|
||||||
|
export const sending = {
|
||||||
|
get value() {
|
||||||
|
return sendingState;
|
||||||
|
},
|
||||||
|
set value(val) {
|
||||||
|
sendingState = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const previewContent = {
|
||||||
|
get value() {
|
||||||
|
return previewContentState;
|
||||||
|
},
|
||||||
|
set value(val) {
|
||||||
|
previewContentState = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const textareaScrollTop = {
|
||||||
|
get value() {
|
||||||
|
return textareaScrollTopState;
|
||||||
|
},
|
||||||
|
set value(val) {
|
||||||
|
textareaScrollTopState = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,3 +3,54 @@ import { dev } from "$app/environment";
|
|||||||
export const API_BASE_PREFIX = dev
|
export const API_BASE_PREFIX = dev
|
||||||
? "http://localhost:3000"
|
? "http://localhost:3000"
|
||||||
: `${process.env.ORIGIN ? `${process.env.ORIGIN}/api` : "http://localhost:3000"}`;
|
: `${process.env.ORIGIN ? `${process.env.ORIGIN}/api` : "http://localhost:3000"}`;
|
||||||
|
|
||||||
|
export const REGISTRATION_IS_DISABLED = dev
|
||||||
|
? false
|
||||||
|
: process.env.REGISTRATION_IS_DISABLED
|
||||||
|
? JSON.parse(process.env.REGISTRATION_IS_DISABLED)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
export const apiRequest = async (
|
||||||
|
customFetch: typeof fetch,
|
||||||
|
url: string,
|
||||||
|
method: "HEAD" | "GET" | "POST" | "PATCH" | "DELETE",
|
||||||
|
options: {
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
body?: any;
|
||||||
|
successMessage?: string;
|
||||||
|
returnData?: boolean;
|
||||||
|
} = {
|
||||||
|
headers: {},
|
||||||
|
body: undefined,
|
||||||
|
successMessage: "Operation was successful",
|
||||||
|
returnData: false
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await customFetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
...(!["HEAD", "GET", "DELETE"].includes(method) && {
|
||||||
|
body: options.body instanceof ArrayBuffer ? options.body : JSON.stringify(options.body)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
return { success: false, message: errorData.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.returnData) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: options.successMessage,
|
||||||
|
data: method === "HEAD" ? response : await response.json()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: options.successMessage };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { WebsiteOverview } from "../../utils";
|
import { type WebsiteOverview, md } from "../../utils";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
websiteOverview,
|
websiteOverview,
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<small>
|
<small>
|
||||||
{@html websiteOverview.footer.additional_text.replace(
|
{@html md(websiteOverview.footer.additional_text, false).replace(
|
||||||
"!!legal",
|
"!!legal",
|
||||||
`<a href="${isIndexPage ? "./legal-information" : "../legal-information"}">Legal information</a>`
|
`<a href="${isIndexPage ? "./legal-information" : "../legal-information"}">Legal information</a>`
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
@@ -29,5 +28,4 @@
|
|||||||
href="{apiUrl}/rpc/retrieve_file?id={websiteOverview.settings.favicon_image}"
|
href="{apiUrl}/rpc/retrieve_file?id={websiteOverview.settings.favicon_image}"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</head>
|
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|||||||
@@ -12,10 +12,20 @@ import type {
|
|||||||
Footer,
|
Footer,
|
||||||
Article,
|
Article,
|
||||||
DocsCategory,
|
DocsCategory,
|
||||||
LegalInformation
|
LegalInformation,
|
||||||
|
DomainPrefix
|
||||||
} from "$lib/db-schema";
|
} from "$lib/db-schema";
|
||||||
|
import type { SubmitFunction } from "@sveltejs/kit";
|
||||||
|
import { sending } from "./runes.svelte";
|
||||||
|
|
||||||
export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"];
|
export const ALLOWED_MIME_TYPES = [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
"image/avif",
|
||||||
|
"image/gif",
|
||||||
|
"image/svg+xml"
|
||||||
|
];
|
||||||
|
|
||||||
export const slugify = (string: string) => {
|
export const slugify = (string: string) => {
|
||||||
return string
|
return string
|
||||||
@@ -24,8 +34,8 @@ export const slugify = (string: string) => {
|
|||||||
.toLowerCase() // Convert to lowercase
|
.toLowerCase() // Convert to lowercase
|
||||||
.trim() // Trim leading and trailing whitespace
|
.trim() // Trim leading and trailing whitespace
|
||||||
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
||||||
.replace(/[^\w\-]+/g, "") // Remove non-word characters (except hyphens)
|
.replace(/[^\w-]+/g, "") // Remove non-word characters (except hyphens)
|
||||||
.replace(/\-\-+/g, "-") // Replace multiple hyphens with single hyphen
|
.replace(/-+/g, "-") // Replace multiple hyphens with single hyphen
|
||||||
.replace(/^-+/, "") // Remove leading hyphens
|
.replace(/^-+/, "") // Remove leading hyphens
|
||||||
.replace(/-+$/, ""); // Remove trailing hyphens
|
.replace(/-+$/, ""); // Remove trailing hyphens
|
||||||
};
|
};
|
||||||
@@ -51,8 +61,8 @@ const createMarkdownParser = (showToc = true) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const gfmHeadingId = ({ prefix = "", showToc = true } = {}) => {
|
const gfmHeadingId = ({ prefix = "", showToc = true } = {}) => {
|
||||||
let headings: { text: string; level: number; id: string }[] = [];
|
const headings: { text: string; level: number; id: string }[] = [];
|
||||||
let sectionStack: { level: number; id: string }[] = [];
|
const sectionStack: { level: number; id: string }[] = [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
renderer: {
|
renderer: {
|
||||||
@@ -143,45 +153,59 @@ export const md = (markdownContent: string, showToc = true) => {
|
|||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: string) => {
|
export const LOADING_DELAY = 500;
|
||||||
const clipboardItems = Array.from(event.clipboardData?.items ?? []);
|
let loadingDelay: number;
|
||||||
const file = clipboardItems.find((item) => item.type.startsWith("image/"));
|
|
||||||
|
|
||||||
if (!file) return null;
|
export const enhanceForm = (options?: {
|
||||||
|
reset?: boolean;
|
||||||
|
closeModal?: boolean;
|
||||||
|
}): SubmitFunction => {
|
||||||
|
return () => {
|
||||||
|
loadingDelay = window.setTimeout(() => (sending.value = true), LOADING_DELAY);
|
||||||
|
|
||||||
event.preventDefault();
|
return async ({ update }) => {
|
||||||
|
await update({ reset: options?.reset ?? true });
|
||||||
const fileObject = file.getAsFile();
|
window.clearTimeout(loadingDelay);
|
||||||
|
if (options?.closeModal) {
|
||||||
if (!fileObject) return;
|
window.location.hash = "!";
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", fileObject);
|
|
||||||
|
|
||||||
const request = await fetch("?/pasteImage", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = deserialize(await request.clone().text());
|
|
||||||
applyAction(result);
|
|
||||||
|
|
||||||
const response = await request.json();
|
|
||||||
|
|
||||||
if (JSON.parse(response.data)[1]) {
|
|
||||||
const fileId = JSON.parse(response.data)[3];
|
|
||||||
const fileUrl = `${API_BASE_PREFIX}/rpc/retrieve_file?id=${fileId}`;
|
|
||||||
|
|
||||||
const target = event.target as HTMLTextAreaElement;
|
|
||||||
const newContent =
|
|
||||||
target.value.slice(0, target.selectionStart) +
|
|
||||||
`` +
|
|
||||||
target.value.slice(target.selectionStart);
|
|
||||||
|
|
||||||
return newContent;
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
sending.value = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hexToHSL = (hex: string) => {
|
||||||
|
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
||||||
|
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
||||||
|
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
||||||
|
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
const d = max - min;
|
||||||
|
|
||||||
|
let h = 0;
|
||||||
|
const l = (max + min) / 2;
|
||||||
|
const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
|
||||||
|
|
||||||
|
if (d !== 0) {
|
||||||
|
switch (max) {
|
||||||
|
case r:
|
||||||
|
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
||||||
|
break;
|
||||||
|
case g:
|
||||||
|
h = ((b - r) / d + 2) / 6;
|
||||||
|
break;
|
||||||
|
case b:
|
||||||
|
h = ((r - g) / d + 4) / 6;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
h: Math.round(h * 360),
|
||||||
|
s: Math.round(s * 100),
|
||||||
|
l: Math.round(l * 100)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface WebsiteOverview extends Website {
|
export interface WebsiteOverview extends Website {
|
||||||
@@ -191,4 +215,5 @@ export interface WebsiteOverview extends Website {
|
|||||||
footer: Footer;
|
footer: Footer;
|
||||||
article: (Article & { docs_category: DocsCategory | null })[];
|
article: (Article & { docs_category: DocsCategory | null })[];
|
||||||
legal_information?: LegalInformation;
|
legal_information?: LegalInformation;
|
||||||
|
domain_prefix?: DomainPrefix;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
import type { Actions } from "./$types";
|
import type { Actions } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
default: async ({ request, cookies, fetch }) => {
|
default: async ({ request, cookies, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/rpc/login`, {
|
const response = await apiRequest(fetch, `${API_BASE_PREFIX}/rpc/login`, "POST", {
|
||||||
method: "POST",
|
body: {
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: data.get("username"),
|
username: data.get("username"),
|
||||||
pass: data.get("password")
|
pass: data.get("password")
|
||||||
})
|
},
|
||||||
|
returnData: true,
|
||||||
|
successMessage: "Successfully logged in, you can refresh the page"
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await res.json();
|
if (!response.success) {
|
||||||
|
return response;
|
||||||
if (!res.ok) {
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cookies.set("session_token", response.token, { path: "/" });
|
cookies.set("session_token", response.data.token, { path: "/" });
|
||||||
return { success: true, message: "Successfully logged in" };
|
return response;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,28 +3,19 @@
|
|||||||
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
||||||
import type { ActionData } from "./$types";
|
import type { ActionData } from "./$types";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
|
import { sending } from "$lib/runes.svelte";
|
||||||
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
|
||||||
const { form }: { form: ActionData } = $props();
|
const { form }: { form: ActionData } = $props();
|
||||||
|
|
||||||
let sending = $state(false);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form
|
<form method="POST" use:enhance={enhanceForm()}>
|
||||||
method="POST"
|
|
||||||
use:enhance={() => {
|
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label>
|
<label>
|
||||||
Username:
|
Username:
|
||||||
<input type="text" name="username" required />
|
<input type="text" name="username" required />
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
import type { Actions } from "./$types";
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX, REGISTRATION_IS_DISABLED, apiRequest } from "$lib/server/utils";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async () => {
|
||||||
|
return {
|
||||||
|
REGISTRATION_IS_DISABLED
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
default: async ({ request, fetch }) => {
|
default: async ({ request, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/rpc/register`, {
|
return await apiRequest(fetch, `${API_BASE_PREFIX}/rpc/register`, "POST", {
|
||||||
method: "POST",
|
body: {
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: data.get("username"),
|
username: data.get("username"),
|
||||||
pass: data.get("password")
|
pass: data.get("password")
|
||||||
})
|
},
|
||||||
|
successMessage: "Successfully registered, you can now login"
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await res.json();
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully registered, you can now login" };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,30 +1,40 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
||||||
import type { ActionData } from "./$types";
|
import type { ActionData, PageServerData } from "./$types";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
|
import { sending } from "$lib/runes.svelte";
|
||||||
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
|
||||||
const { form }: { form: ActionData } = $props();
|
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||||
|
|
||||||
let sending = $state(false);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form
|
{#if data.REGISTRATION_IS_DISABLED}
|
||||||
method="POST"
|
<p class="registration-disabled">
|
||||||
use:enhance={() => {
|
<svg
|
||||||
sending = true;
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
return async ({ update }) => {
|
viewBox="0 0 20 20"
|
||||||
await update();
|
fill="currentColor"
|
||||||
sending = false;
|
width="20"
|
||||||
};
|
height="20"
|
||||||
}}
|
color="var(--color-error)"
|
||||||
>
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Account registration is disabled on this instance
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<form method="POST" use:enhance={enhanceForm()}>
|
||||||
<label>
|
<label>
|
||||||
Username:
|
Username:
|
||||||
<input type="text" name="username" minlength="3" maxlength="16" required />
|
<input type="text" name="username" minlength="3" maxlength="16" required />
|
||||||
@@ -35,4 +45,13 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.registration-disabled {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { Actions, PageServerLoad } from "./$types";
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
|
import { apiRequest } from "$lib/server/utils";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||||
import { rm } from "node:fs/promises";
|
import { rm } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { Website, WebsiteInput } from "$lib/db-schema";
|
import type { Website } from "$lib/db-schema";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => {
|
export const load: PageServerLoad = async ({ fetch, url, locals }) => {
|
||||||
const searchQuery = url.searchParams.get("website_search_query");
|
const searchQuery = url.searchParams.get("website_search_query");
|
||||||
const filterBy = url.searchParams.get("website_filter");
|
const filterBy = url.searchParams.get("website_filter");
|
||||||
|
|
||||||
@@ -27,28 +28,22 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => {
|
|||||||
|
|
||||||
const constructedFetchUrl = `${baseFetchUrl}&${params.toString()}`;
|
const constructedFetchUrl = `${baseFetchUrl}&${params.toString()}`;
|
||||||
|
|
||||||
const totalWebsitesData = await fetch(baseFetchUrl, {
|
const totalWebsites = await apiRequest(fetch, baseFetchUrl, "HEAD", {
|
||||||
method: "HEAD",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Prefer: "count=exact"
|
Prefer: "count=exact"
|
||||||
}
|
},
|
||||||
|
returnData: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalWebsiteCount = Number(
|
const totalWebsiteCount = Number(
|
||||||
totalWebsitesData.headers.get("content-range")?.split("/").at(-1)
|
totalWebsites.data.headers.get("content-range")?.split("/").at(-1)
|
||||||
);
|
);
|
||||||
|
|
||||||
const websiteData = await fetch(constructedFetchUrl, {
|
const websites: Website[] = (
|
||||||
method: "GET",
|
await apiRequest(fetch, constructedFetchUrl, "GET", {
|
||||||
headers: {
|
returnData: true
|
||||||
"Content-Type": "application/json",
|
})
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
).data;
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const websites: Website[] = await websiteData.json();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalWebsiteCount,
|
totalWebsiteCount,
|
||||||
@@ -57,70 +52,63 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
createWebsite: async ({ request, fetch, cookies }) => {
|
createWebsite: async ({ request, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/rpc/create_website`, {
|
return await apiRequest(fetch, `${API_BASE_PREFIX}/rpc/create_website`, "POST", {
|
||||||
method: "POST",
|
body: {
|
||||||
headers: {
|
content_type: data.get("content-type"),
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
content_type: data.get("content-type") as string,
|
|
||||||
title: data.get("title") as string
|
|
||||||
} satisfies WebsiteInput)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully created website" };
|
|
||||||
},
|
|
||||||
updateWebsite: async ({ request, cookies, fetch }) => {
|
|
||||||
const data = await request.formData();
|
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/website?id=eq.${data.get("id")}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
title: data.get("title")
|
title: data.get("title")
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully updated website" };
|
|
||||||
},
|
},
|
||||||
deleteWebsite: async ({ request, cookies, fetch }) => {
|
successMessage: "Successfully created website"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateWebsite: async ({ request, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/website?id=eq.${data.get("id")}`, {
|
return await apiRequest(fetch, `${API_BASE_PREFIX}/website?id=eq.${data.get("id")}`, "PATCH", {
|
||||||
method: "DELETE",
|
body: {
|
||||||
headers: {
|
title: data.get("title")
|
||||||
"Content-Type": "application/json",
|
},
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
successMessage: "Successfully updated website"
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
deleteWebsite: async ({ request, fetch }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const id = data.get("id");
|
||||||
|
|
||||||
if (!res.ok) {
|
const oldDomainPrefix = (
|
||||||
const response = await res.json();
|
await apiRequest(fetch, `${API_BASE_PREFIX}/domain_prefix?website_id=eq.${id}`, "GET", {
|
||||||
return { success: false, message: response.message };
|
headers: {
|
||||||
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
|
},
|
||||||
|
returnData: true
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
const deleteWebsite = await apiRequest(
|
||||||
|
fetch,
|
||||||
|
`${API_BASE_PREFIX}/website?id=eq.${id}`,
|
||||||
|
"DELETE",
|
||||||
|
{
|
||||||
|
successMessage: "Successfully deleted website"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!deleteWebsite.success) {
|
||||||
|
return deleteWebsite;
|
||||||
}
|
}
|
||||||
|
|
||||||
await rm(join("/", "var", "www", "archtika-websites", data.get("id") as string), {
|
await rm(join("/", "var", "www", "archtika-websites", "previews", id as string), {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
force: true
|
force: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true, message: "Successfully deleted website" };
|
await rm(join("/", "var", "www", "archtika-websites", oldDomainPrefix?.prefix ?? id), {
|
||||||
|
recursive: true,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return deleteWebsite;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,15 +6,15 @@
|
|||||||
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
||||||
import type { ActionData, PageServerData } from "./$types";
|
import type { ActionData, PageServerData } from "./$types";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
import { sending } from "$lib/runes.svelte";
|
||||||
|
|
||||||
const { form, data }: { form: ActionData; data: PageServerData } = $props();
|
const { form, data }: { form: ActionData; data: PageServerData } = $props();
|
||||||
|
|
||||||
let sending = $state(false);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -26,18 +26,7 @@
|
|||||||
<Modal id="create-website" text="Create website">
|
<Modal id="create-website" text="Create website">
|
||||||
<h3>Create website</h3>
|
<h3>Create website</h3>
|
||||||
|
|
||||||
<form
|
<form method="POST" action="?/createWebsite" use:enhance={enhanceForm({ closeModal: true })}>
|
||||||
method="POST"
|
|
||||||
action="?/createWebsite"
|
|
||||||
use:enhance={() => {
|
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label>
|
<label>
|
||||||
Type:
|
Type:
|
||||||
<select name="content-type">
|
<select name="content-type">
|
||||||
@@ -119,14 +108,7 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/updateWebsite"
|
action="?/updateWebsite"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ reset: false, closeModal: true })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update({ reset: false });
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={id} />
|
<input type="hidden" name="id" value={id} />
|
||||||
<label>
|
<label>
|
||||||
@@ -154,14 +136,7 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/deleteWebsite"
|
action="?/deleteWebsite"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ closeModal: true })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={id} />
|
<input type="hidden" name="id" value={id} />
|
||||||
|
|
||||||
@@ -179,7 +154,7 @@
|
|||||||
.website-grid {
|
.website-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-s);
|
gap: var(--space-s);
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 35ch), 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 35ch), 0.5fr));
|
||||||
margin-block-start: var(--space-xs);
|
margin-block-start: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Actions, PageServerLoad } from "./$types";
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals }) => {
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
return {
|
return {
|
||||||
@@ -16,24 +16,18 @@ export const actions: Actions = {
|
|||||||
deleteAccount: async ({ request, fetch, cookies }) => {
|
deleteAccount: async ({ request, fetch, cookies }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/rpc/delete_account`, {
|
const response = await apiRequest(fetch, `${API_BASE_PREFIX}/rpc/delete_account`, "POST", {
|
||||||
method: "POST",
|
body: {
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
pass: data.get("password")
|
pass: data.get("password")
|
||||||
})
|
},
|
||||||
|
successMessage: "Successfully deleted account"
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await res.json();
|
if (!response.success) {
|
||||||
|
return response;
|
||||||
if (!res.ok) {
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cookies.delete("session_token", { path: "/" });
|
cookies.delete("session_token", { path: "/" });
|
||||||
return { success: true, message: "Successfully deleted account" };
|
return response;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
import type { ActionData, PageServerData } from "./$types";
|
import type { ActionData, PageServerData } from "./$types";
|
||||||
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
import { sending } from "$lib/runes.svelte";
|
||||||
|
|
||||||
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||||
|
|
||||||
let sending = $state(false);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -38,17 +38,7 @@
|
|||||||
<a href="#logout">Logout</a>
|
<a href="#logout">Logout</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<form
|
<form method="POST" action="?/logout" use:enhance={enhanceForm()}>
|
||||||
method="POST"
|
|
||||||
action="?/logout"
|
|
||||||
use:enhance={() => {
|
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button type="submit">Logout</button>
|
<button type="submit">Logout</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@@ -66,18 +56,7 @@
|
|||||||
Deleting your account will irretrievably erase all data.
|
Deleting your account will irretrievably erase all data.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form
|
<form method="POST" action="?/deleteAccount" use:enhance={enhanceForm({ closeModal: true })}>
|
||||||
method="POST"
|
|
||||||
action="?/deleteAccount"
|
|
||||||
use:enhance={() => {
|
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label>
|
<label>
|
||||||
Password:
|
Password:
|
||||||
<input type="password" name="password" required />
|
<input type="password" name="password" required />
|
||||||
@@ -87,3 +66,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form[action="?/logout"] > button {
|
||||||
|
max-inline-size: fit-content;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,36 +1,35 @@
|
|||||||
import type { LayoutServerLoad } from "./$types";
|
import type { LayoutServerLoad } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import type { Website, Home, User } from "$lib/db-schema";
|
import type { Website, Home, User } from "$lib/db-schema";
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({ params, fetch, cookies }) => {
|
export const load: LayoutServerLoad = async ({ params, fetch }) => {
|
||||||
const websiteData = await fetch(
|
const websiteData = await apiRequest(
|
||||||
|
fetch,
|
||||||
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,user!user_id(username)`,
|
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,user!user_id(username)`,
|
||||||
|
"GET",
|
||||||
{
|
{
|
||||||
method: "GET",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
}
|
},
|
||||||
|
returnData: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!websiteData.ok) {
|
const website: Website & { user: { username: User["username"] } } = websiteData.data;
|
||||||
|
|
||||||
|
if (!websiteData.success) {
|
||||||
throw error(404, "Website not found");
|
throw error(404, "Website not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const homeData = await fetch(`${API_BASE_PREFIX}/home?website_id=eq.${params.websiteId}`, {
|
const home: Home = (
|
||||||
method: "GET",
|
await apiRequest(fetch, `${API_BASE_PREFIX}/home?website_id=eq.${params.websiteId}`, "GET", {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
}
|
},
|
||||||
});
|
returnData: true
|
||||||
|
})
|
||||||
const website: Website & { user: { username: User["username"] } } = await websiteData.json();
|
).data;
|
||||||
const home: Home = await homeData.json();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
website,
|
website,
|
||||||
|
|||||||
@@ -1,41 +1,40 @@
|
|||||||
import type { Actions, PageServerLoad } from "./$types";
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||||
|
import { apiRequest } from "$lib/server/utils";
|
||||||
import type { Settings, Header, Footer } from "$lib/db-schema";
|
import type { Settings, Header, Footer } from "$lib/db-schema";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
|
export const load: PageServerLoad = async ({ params, fetch }) => {
|
||||||
const globalSettingsData = await fetch(
|
const globalSettings: Settings = (
|
||||||
|
await apiRequest(
|
||||||
|
fetch,
|
||||||
`${API_BASE_PREFIX}/settings?website_id=eq.${params.websiteId}`,
|
`${API_BASE_PREFIX}/settings?website_id=eq.${params.websiteId}`,
|
||||||
|
"GET",
|
||||||
{
|
{
|
||||||
method: "GET",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
|
},
|
||||||
|
returnData: true
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
);
|
).data;
|
||||||
|
|
||||||
const headerData = await fetch(`${API_BASE_PREFIX}/header?website_id=eq.${params.websiteId}`, {
|
const header: Header = (
|
||||||
method: "GET",
|
await apiRequest(fetch, `${API_BASE_PREFIX}/header?website_id=eq.${params.websiteId}`, "GET", {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
}
|
},
|
||||||
});
|
returnData: true
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
const footerData = await fetch(`${API_BASE_PREFIX}/footer?website_id=eq.${params.websiteId}`, {
|
const footer: Footer = (
|
||||||
method: "GET",
|
await apiRequest(fetch, `${API_BASE_PREFIX}/footer?website_id=eq.${params.websiteId}`, "GET", {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
}
|
},
|
||||||
});
|
returnData: true
|
||||||
|
})
|
||||||
const globalSettings: Settings = await globalSettingsData.json();
|
).data;
|
||||||
const header: Header = await headerData.json();
|
|
||||||
const footer: Footer = await footerData.json();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
globalSettings,
|
globalSettings,
|
||||||
@@ -46,13 +45,12 @@ export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
updateGlobal: async ({ request, fetch, cookies, params }) => {
|
updateGlobal: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const faviconFile = data.get("favicon") as File;
|
const faviconFile = data.get("favicon") as File;
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json",
|
Accept: "application/vnd.pgrst.object+json",
|
||||||
"X-Website-Id": params.websiteId
|
"X-Website-Id": params.websiteId
|
||||||
};
|
};
|
||||||
@@ -62,48 +60,38 @@ export const actions: Actions = {
|
|||||||
headers["X-Original-Filename"] = faviconFile.name;
|
headers["X-Original-Filename"] = faviconFile.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedImageData = await fetch(`${API_BASE_PREFIX}/rpc/upload_file`, {
|
const uploadedImage = await apiRequest(fetch, `${API_BASE_PREFIX}/rpc/upload_file`, "POST", {
|
||||||
method: "POST",
|
|
||||||
headers,
|
headers,
|
||||||
body: faviconFile ? await faviconFile.arrayBuffer() : null
|
body: faviconFile ? await faviconFile.arrayBuffer() : null,
|
||||||
|
returnData: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadedImage = await uploadedImageData.json();
|
if (!uploadedImage.success && (faviconFile?.size ?? 0 > 0)) {
|
||||||
|
return uploadedImage;
|
||||||
if (!uploadedImageData.ok && (faviconFile?.size ?? 0 > 0)) {
|
|
||||||
return { success: false, message: uploadedImage.message };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/settings?website_id=eq.${params.websiteId}`, {
|
return await apiRequest(
|
||||||
method: "PATCH",
|
fetch,
|
||||||
headers: {
|
`${API_BASE_PREFIX}/settings?website_id=eq.${params.websiteId}`,
|
||||||
"Content-Type": "application/json",
|
"PATCH",
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
{
|
||||||
},
|
body: {
|
||||||
body: JSON.stringify({
|
|
||||||
accent_color_light_theme: data.get("accent-color-light"),
|
|
||||||
accent_color_dark_theme: data.get("accent-color-dark"),
|
accent_color_dark_theme: data.get("accent-color-dark"),
|
||||||
favicon_image: uploadedImage.file_id
|
accent_color_light_theme: data.get("accent-color-light"),
|
||||||
})
|
background_color_dark_theme: data.get("background-color-dark"),
|
||||||
});
|
background_color_light_theme: data.get("background-color-light"),
|
||||||
|
favicon_image: uploadedImage.data?.file_id
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Successfully updated global settings"
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
updateHeader: async ({ request, fetch, cookies, params }) => {
|
successMessage: "Successfully updated global settings"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateHeader: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const logoImage = data.get("logo-image") as File;
|
const logoImage = data.get("logo-image") as File;
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json",
|
Accept: "application/vnd.pgrst.object+json",
|
||||||
"X-Website-Id": params.websiteId
|
"X-Website-Id": params.websiteId
|
||||||
};
|
};
|
||||||
@@ -113,109 +101,75 @@ export const actions: Actions = {
|
|||||||
headers["X-Original-Filename"] = logoImage.name;
|
headers["X-Original-Filename"] = logoImage.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedImageData = await fetch(`${API_BASE_PREFIX}/rpc/upload_file`, {
|
const uploadedImage = await apiRequest(fetch, `${API_BASE_PREFIX}/rpc/upload_file`, "POST", {
|
||||||
method: "POST",
|
|
||||||
headers,
|
headers,
|
||||||
body: logoImage ? await logoImage.arrayBuffer() : null
|
body: logoImage ? await logoImage.arrayBuffer() : null,
|
||||||
|
returnData: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadedImage = await uploadedImageData.json();
|
if (!uploadedImage.success && (logoImage?.size ?? 0 > 0)) {
|
||||||
|
|
||||||
if (!uploadedImageData.ok && (logoImage?.size ?? 0 > 0)) {
|
|
||||||
return { success: false, message: uploadedImage.message };
|
return { success: false, message: uploadedImage.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/header?website_id=eq.${params.websiteId}`, {
|
return await apiRequest(
|
||||||
method: "PATCH",
|
fetch,
|
||||||
headers: {
|
`${API_BASE_PREFIX}/header?website_id=eq.${params.websiteId}`,
|
||||||
"Content-Type": "application/json",
|
"PATCH",
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
{
|
||||||
},
|
body: {
|
||||||
body: JSON.stringify({
|
|
||||||
logo_type: data.get("logo-type"),
|
logo_type: data.get("logo-type"),
|
||||||
logo_text: data.get("logo-text"),
|
logo_text: data.get("logo-text"),
|
||||||
logo_image: uploadedImage.file_id
|
logo_image: uploadedImage.data?.file_id
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Successfully updated header"
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
updateHome: async ({ request, fetch, cookies, params }) => {
|
successMessage: "Successfully updated header"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateHome: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/home?website_id=eq.${params.websiteId}`, {
|
return await apiRequest(
|
||||||
method: "PATCH",
|
fetch,
|
||||||
headers: {
|
`${API_BASE_PREFIX}/home?website_id=eq.${params.websiteId}`,
|
||||||
"Content-Type": "application/json",
|
"PATCH",
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
{
|
||||||
},
|
body: {
|
||||||
body: JSON.stringify({
|
|
||||||
main_content: data.get("main-content")
|
main_content: data.get("main-content")
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully updated home" };
|
|
||||||
},
|
},
|
||||||
updateFooter: async ({ request, fetch, cookies, params }) => {
|
successMessage: "Successfully updated home"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateFooter: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/footer?website_id=eq.${params.websiteId}`, {
|
return await apiRequest(
|
||||||
method: "PATCH",
|
fetch,
|
||||||
headers: {
|
`${API_BASE_PREFIX}/footer?website_id=eq.${params.websiteId}`,
|
||||||
"Content-Type": "application/json",
|
"PATCH",
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
{
|
||||||
},
|
body: {
|
||||||
body: JSON.stringify({
|
|
||||||
additional_text: data.get("additional-text")
|
additional_text: data.get("additional-text")
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Successfully updated footer"
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
pasteImage: async ({ request, fetch, cookies, params }) => {
|
successMessage: "Successfully updated footer"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pasteImage: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const file = data.get("file") as File;
|
const file = data.get("file") as File;
|
||||||
|
|
||||||
const fileData = await fetch(`${API_BASE_PREFIX}/rpc/upload_file`, {
|
return await apiRequest(fetch, `${API_BASE_PREFIX}/rpc/upload_file`, "POST", {
|
||||||
method: "POST",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json",
|
Accept: "application/vnd.pgrst.object+json",
|
||||||
"X-Website-Id": params.websiteId,
|
"X-Website-Id": params.websiteId,
|
||||||
"X-Mimetype": file.type,
|
"X-Mimetype": file.type,
|
||||||
"X-Original-Filename": file.name
|
"X-Original-Filename": file.name
|
||||||
},
|
},
|
||||||
body: await file.arrayBuffer()
|
body: await file.arrayBuffer(),
|
||||||
|
successMessage: "Successfully uploaded image",
|
||||||
|
returnData: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileJSON = await fileData.json();
|
|
||||||
|
|
||||||
if (!fileData.ok) {
|
|
||||||
return { success: false, message: fileJSON.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully uploaded image", fileId: fileJSON.file_id };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,37 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
import WebsiteEditor from "$lib/components/WebsiteEditor.svelte";
|
import WebsiteEditor from "$lib/components/WebsiteEditor.svelte";
|
||||||
import { ALLOWED_MIME_TYPES, handleImagePaste } from "$lib/utils";
|
import { ALLOWED_MIME_TYPES } from "$lib/utils";
|
||||||
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
||||||
import type { ActionData, LayoutServerData, PageServerData } from "./$types";
|
import type { ActionData, LayoutServerData, PageServerData } from "./$types";
|
||||||
import Modal from "$lib/components/Modal.svelte";
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
import { sending } from "$lib/runes.svelte";
|
||||||
|
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
|
||||||
|
import { previewContent } from "$lib/runes.svelte";
|
||||||
|
|
||||||
const { data, form }: { data: PageServerData & LayoutServerData; form: ActionData } = $props();
|
const { data, form }: { data: PageServerData & LayoutServerData; form: ActionData } = $props();
|
||||||
|
|
||||||
let previewContent = $state(data.home.main_content);
|
previewContent.value = data.home.main_content;
|
||||||
let mainContentTextarea: HTMLTextAreaElement;
|
|
||||||
let textareaScrollTop = $state(0);
|
|
||||||
|
|
||||||
const updateScrollPercentage = () => {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = mainContentTextarea;
|
|
||||||
textareaScrollTop = (scrollTop / (scrollHeight - clientHeight)) * 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePaste = async (event: ClipboardEvent) => {
|
|
||||||
const newContent = await handleImagePaste(event, data.API_BASE_PREFIX);
|
|
||||||
|
|
||||||
if (newContent) {
|
|
||||||
previewContent = newContent;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let sending = $state(false);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -39,9 +26,6 @@
|
|||||||
id={data.website.id}
|
id={data.website.id}
|
||||||
contentType={data.website.content_type}
|
contentType={data.website.content_type}
|
||||||
title={data.website.title}
|
title={data.website.title}
|
||||||
previewContent={previewContent ||
|
|
||||||
"Put some markdown content in main content to see a live preview here"}
|
|
||||||
previewScrollTop={textareaScrollTop}
|
|
||||||
>
|
>
|
||||||
<section id="global">
|
<section id="global">
|
||||||
<h2>
|
<h2>
|
||||||
@@ -51,26 +35,30 @@
|
|||||||
action="?/updateGlobal"
|
action="?/updateGlobal"
|
||||||
method="POST"
|
method="POST"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ reset: false })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update({ reset: false });
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<label>
|
<label>
|
||||||
Light accent color:
|
Background color dark theme:
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
name="accent-color-light"
|
name="background-color-dark"
|
||||||
value={data.globalSettings.accent_color_light_theme}
|
value={data.globalSettings.background_color_dark_theme}
|
||||||
pattern="\S(.*\S)?"
|
pattern="\S(.*\S)?"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Dark accent color:
|
Background color light theme:
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
name="background-color-light"
|
||||||
|
value={data.globalSettings.background_color_light_theme}
|
||||||
|
pattern="\S(.*\S)?"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Accent color dark theme:
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
name="accent-color-dark"
|
name="accent-color-dark"
|
||||||
@@ -79,6 +67,16 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
Accent color light theme:
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
name="accent-color-light"
|
||||||
|
value={data.globalSettings.accent_color_light_theme}
|
||||||
|
pattern="\S(.*\S)?"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<div class="file-field">
|
<div class="file-field">
|
||||||
<label>
|
<label>
|
||||||
Favicon:
|
Favicon:
|
||||||
@@ -107,13 +105,7 @@
|
|||||||
action="?/updateHeader"
|
action="?/updateHeader"
|
||||||
method="POST"
|
method="POST"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ reset: false })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update({ reset: false });
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<label>
|
<label>
|
||||||
Logo type:
|
Logo type:
|
||||||
@@ -156,29 +148,13 @@
|
|||||||
<a href="#home">Home</a>
|
<a href="#home">Home</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<form
|
<form action="?/updateHome" method="POST" use:enhance={enhanceForm({ reset: false })}>
|
||||||
action="?/updateHome"
|
<MarkdownEditor
|
||||||
method="POST"
|
apiPrefix={data.API_BASE_PREFIX}
|
||||||
use:enhance={() => {
|
label="Main content"
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update({ reset: false });
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label>
|
|
||||||
Main content:
|
|
||||||
<textarea
|
|
||||||
name="main-content"
|
name="main-content"
|
||||||
rows="20"
|
content={data.home.main_content}
|
||||||
bind:value={previewContent}
|
/>
|
||||||
bind:this={mainContentTextarea}
|
|
||||||
onscroll={updateScrollPercentage}
|
|
||||||
onpaste={handlePaste}
|
|
||||||
required>{data.home.main_content}</textarea
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -189,17 +165,7 @@
|
|||||||
<a href="#footer">Footer</a>
|
<a href="#footer">Footer</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<form
|
<form action="?/updateFooter" method="POST" use:enhance={enhanceForm({ reset: false })}>
|
||||||
action="?/updateFooter"
|
|
||||||
method="POST"
|
|
||||||
use:enhance={() => {
|
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update({ reset: false });
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label>
|
<label>
|
||||||
Additional text:
|
Additional text:
|
||||||
<textarea name="additional-text" rows="5" maxlength="250" required
|
<textarea name="additional-text" rows="5" maxlength="250" required
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { Actions, PageServerLoad } from "./$types";
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||||
import type { Article, ArticleInput, DocsCategory } from "$lib/db-schema";
|
import { apiRequest } from "$lib/server/utils";
|
||||||
|
import type { Article, DocsCategory } from "$lib/db-schema";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, fetch, cookies, url, parent, locals }) => {
|
export const load: PageServerLoad = async ({ params, fetch, url, parent, locals }) => {
|
||||||
const searchQuery = url.searchParams.get("article_search_query");
|
const searchQuery = url.searchParams.get("article_search_query");
|
||||||
const filterBy = url.searchParams.get("article_filter");
|
const filterBy = url.searchParams.get("article_filter");
|
||||||
|
|
||||||
@@ -34,28 +35,22 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, url, parent
|
|||||||
|
|
||||||
const constructedFetchUrl = `${baseFetchUrl}&${parameters.toString()}`;
|
const constructedFetchUrl = `${baseFetchUrl}&${parameters.toString()}`;
|
||||||
|
|
||||||
const totalArticlesData = await fetch(baseFetchUrl, {
|
const totalArticles = await apiRequest(fetch, baseFetchUrl, "HEAD", {
|
||||||
method: "HEAD",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Prefer: "count=exact"
|
Prefer: "count=exact"
|
||||||
}
|
},
|
||||||
|
returnData: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalArticleCount = Number(
|
const totalArticleCount = Number(
|
||||||
totalArticlesData.headers.get("content-range")?.split("/").at(-1)
|
totalArticles.data.headers.get("content-range")?.split("/").at(-1)
|
||||||
);
|
);
|
||||||
|
|
||||||
const articlesData = await fetch(constructedFetchUrl, {
|
const articles: (Article & { docs_category: DocsCategory | null })[] = (
|
||||||
method: "GET",
|
await apiRequest(fetch, constructedFetchUrl, "GET", {
|
||||||
headers: {
|
returnData: true
|
||||||
"Content-Type": "application/json",
|
})
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
).data;
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const articles: (Article & { docs_category: DocsCategory | null })[] = await articlesData.json();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalArticleCount,
|
totalArticleCount,
|
||||||
@@ -66,44 +61,22 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, url, parent
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
createArticle: async ({ request, fetch, cookies, params }) => {
|
createArticle: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/article`, {
|
return await apiRequest(fetch, `${API_BASE_PREFIX}/article`, "POST", {
|
||||||
method: "POST",
|
body: {
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
website_id: params.websiteId,
|
website_id: params.websiteId,
|
||||||
title: data.get("title") as string
|
title: data.get("title")
|
||||||
} satisfies ArticleInput)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully created article" };
|
|
||||||
},
|
},
|
||||||
deleteArticle: async ({ request, fetch, cookies }) => {
|
successMessage: "Successfully created article"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteArticle: async ({ request, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/article?id=eq.${data.get("id")}`, {
|
return await apiRequest(fetch, `${API_BASE_PREFIX}/article?id=eq.${data.get("id")}`, "DELETE", {
|
||||||
method: "DELETE",
|
successMessage: "Successfully deleted article"
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully deleted article" };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,15 +6,18 @@
|
|||||||
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
import type { ActionData, PageServerData } from "./$types";
|
import type { ActionData, PageServerData } from "./$types";
|
||||||
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
import { sending } from "$lib/runes.svelte";
|
||||||
|
import { previewContent } from "$lib/runes.svelte";
|
||||||
|
|
||||||
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||||
|
|
||||||
let sending = $state(false);
|
previewContent.value = data.home.main_content;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -22,7 +25,6 @@
|
|||||||
id={data.website.id}
|
id={data.website.id}
|
||||||
contentType={data.website.content_type}
|
contentType={data.website.content_type}
|
||||||
title={data.website.title}
|
title={data.website.title}
|
||||||
previewContent={data.home.main_content}
|
|
||||||
>
|
>
|
||||||
<section id="create-article">
|
<section id="create-article">
|
||||||
<h2>
|
<h2>
|
||||||
@@ -32,18 +34,7 @@
|
|||||||
<Modal id="create-article" text="Create article">
|
<Modal id="create-article" text="Create article">
|
||||||
<h3>Create article</h3>
|
<h3>Create article</h3>
|
||||||
|
|
||||||
<form
|
<form method="POST" action="?/createArticle" use:enhance={enhanceForm({ closeModal: true })}>
|
||||||
method="POST"
|
|
||||||
action="?/createArticle"
|
|
||||||
use:enhance={() => {
|
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label>
|
<label>
|
||||||
Title:
|
Title:
|
||||||
<input type="text" name="title" pattern="\S(.*\S)?" maxlength="100" required />
|
<input type="text" name="title" pattern="\S(.*\S)?" maxlength="100" required />
|
||||||
@@ -134,14 +125,7 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/deleteArticle"
|
action="?/deleteArticle"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ closeModal: true })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={id} />
|
<input type="hidden" name="id" value={id} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +1,40 @@
|
|||||||
import type { Actions, PageServerLoad } from "./$types";
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||||
import type { Article, DocsCategory } from "$lib/db-schema";
|
import type { Article, DocsCategory } from "$lib/db-schema";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => {
|
export const load: PageServerLoad = async ({ parent, params, fetch }) => {
|
||||||
const articleData = await fetch(`${API_BASE_PREFIX}/article?id=eq.${params.articleId}`, {
|
const article: Article = (
|
||||||
method: "GET",
|
await apiRequest(fetch, `${API_BASE_PREFIX}/article?id=eq.${params.articleId}`, "GET", {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
}
|
},
|
||||||
});
|
returnData: true
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
const categoryData = await fetch(
|
const categories: DocsCategory[] = (
|
||||||
|
await apiRequest(
|
||||||
|
fetch,
|
||||||
`${API_BASE_PREFIX}/docs_category?website_id=eq.${params.websiteId}&order=category_weight.desc`,
|
`${API_BASE_PREFIX}/docs_category?website_id=eq.${params.websiteId}&order=category_weight.desc`,
|
||||||
|
"GET",
|
||||||
{
|
{
|
||||||
method: "GET",
|
returnData: true
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
);
|
).data;
|
||||||
|
|
||||||
const article: Article = await articleData.json();
|
|
||||||
const categories: DocsCategory[] = await categoryData.json();
|
|
||||||
const { website } = await parent();
|
const { website } = await parent();
|
||||||
|
|
||||||
return { website, article, categories, API_BASE_PREFIX };
|
return { website, article, categories, API_BASE_PREFIX };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
editArticle: async ({ fetch, cookies, request, params }) => {
|
editArticle: async ({ fetch, request, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const coverFile = data.get("cover-image") as File;
|
const coverFile = data.get("cover-image") as File;
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json",
|
Accept: "application/vnd.pgrst.object+json",
|
||||||
"X-Website-Id": params.websiteId
|
"X-Website-Id": params.websiteId
|
||||||
};
|
};
|
||||||
@@ -47,66 +44,50 @@ export const actions: Actions = {
|
|||||||
headers["X-Original-Filename"] = coverFile.name;
|
headers["X-Original-Filename"] = coverFile.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadedImageData = await fetch(`${API_BASE_PREFIX}/rpc/upload_file`, {
|
const uploadedImage = await apiRequest(fetch, `${API_BASE_PREFIX}/rpc/upload_file`, "POST", {
|
||||||
method: "POST",
|
|
||||||
headers,
|
headers,
|
||||||
body: coverFile ? await coverFile.arrayBuffer() : null
|
body: coverFile ? await coverFile.arrayBuffer() : null,
|
||||||
|
returnData: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadedImage = await uploadedImageData.json();
|
if (!uploadedImage.success && (coverFile?.size ?? 0 > 0)) {
|
||||||
|
|
||||||
if (!uploadedImageData.ok && (coverFile?.size ?? 0 > 0)) {
|
|
||||||
return { success: false, message: uploadedImage.message };
|
return { success: false, message: uploadedImage.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/article?id=eq.${params.articleId}`, {
|
return await apiRequest(
|
||||||
method: "PATCH",
|
fetch,
|
||||||
headers: {
|
`${API_BASE_PREFIX}/article?id=eq.${params.articleId}`,
|
||||||
"Content-Type": "application/json",
|
"PATCH",
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
{
|
||||||
},
|
body: {
|
||||||
body: JSON.stringify({
|
|
||||||
title: data.get("title"),
|
title: data.get("title"),
|
||||||
meta_description: data.get("description"),
|
meta_description: data.get("description"),
|
||||||
meta_author: data.get("author"),
|
meta_author: data.get("author"),
|
||||||
cover_image: uploadedImage.file_id,
|
cover_image: uploadedImage.data?.file_id,
|
||||||
publication_date: data.get("publication-date"),
|
publication_date: data.get("publication-date"),
|
||||||
main_content: data.get("main-content"),
|
main_content: data.get("main-content"),
|
||||||
category: data.get("category"),
|
category: data.get("category"),
|
||||||
article_weight: data.get("article-weight") ? data.get("article-weight") : null
|
article_weight: data.get("article-weight") ? data.get("article-weight") : null
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully updated article" };
|
|
||||||
},
|
},
|
||||||
pasteImage: async ({ request, fetch, cookies, params }) => {
|
successMessage: "Successfully updated article"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pasteImage: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const file = data.get("file") as File;
|
const file = data.get("file") as File;
|
||||||
|
|
||||||
const fileData = await fetch(`${API_BASE_PREFIX}/rpc/upload_file`, {
|
return await apiRequest(fetch, `${API_BASE_PREFIX}/rpc/upload_file`, "POST", {
|
||||||
method: "POST",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json",
|
Accept: "application/vnd.pgrst.object+json",
|
||||||
"X-Website-Id": params.websiteId,
|
"X-Website-Id": params.websiteId,
|
||||||
"X-Mimetype": file.type,
|
"X-Mimetype": file.type,
|
||||||
"X-Original-Filename": file.name
|
"X-Original-Filename": file.name
|
||||||
},
|
},
|
||||||
body: await file.arrayBuffer()
|
body: await file.arrayBuffer(),
|
||||||
|
successMessage: "Successfully uploaded image",
|
||||||
|
returnData: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileJSON = await fileData.json();
|
|
||||||
|
|
||||||
if (!fileData.ok) {
|
|
||||||
return { success: false, message: fileJSON.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully uploaded image", fileId: fileJSON.file_id };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,32 +6,19 @@
|
|||||||
import type { ActionData, PageServerData } from "./$types";
|
import type { ActionData, PageServerData } from "./$types";
|
||||||
import Modal from "$lib/components/Modal.svelte";
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
import { handleImagePaste } from "$lib/utils";
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
import { sending } from "$lib/runes.svelte";
|
||||||
|
import { previewContent } from "$lib/runes.svelte";
|
||||||
|
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
|
||||||
|
|
||||||
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||||
|
|
||||||
let previewContent = $state(data.article.main_content);
|
previewContent.value = data.article?.main_content ?? "";
|
||||||
let mainContentTextarea: HTMLTextAreaElement;
|
|
||||||
let textareaScrollTop = $state(0);
|
|
||||||
|
|
||||||
const updateScrollPercentage = () => {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = mainContentTextarea;
|
|
||||||
textareaScrollTop = (scrollTop / (scrollHeight - clientHeight)) * 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePaste = async (event: ClipboardEvent) => {
|
|
||||||
const newContent = await handleImagePaste(event, data.API_BASE_PREFIX);
|
|
||||||
if (newContent) {
|
|
||||||
previewContent = newContent;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let sending = $state(false);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -39,9 +26,6 @@
|
|||||||
id={data.website.id}
|
id={data.website.id}
|
||||||
contentType={data.website.content_type}
|
contentType={data.website.content_type}
|
||||||
title={data.website.title}
|
title={data.website.title}
|
||||||
previewContent={previewContent ||
|
|
||||||
"Put some markdown content in main content to see a live preview here"}
|
|
||||||
previewScrollTop={textareaScrollTop}
|
|
||||||
>
|
>
|
||||||
<section id="edit-article">
|
<section id="edit-article">
|
||||||
<h2>
|
<h2>
|
||||||
@@ -52,13 +36,7 @@
|
|||||||
method="POST"
|
method="POST"
|
||||||
action="?/editArticle"
|
action="?/editArticle"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ reset: false })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update({ reset: false });
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{#if data.website.content_type === "Docs"}
|
{#if data.website.content_type === "Docs"}
|
||||||
<label>
|
<label>
|
||||||
@@ -132,18 +110,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<label>
|
<MarkdownEditor
|
||||||
Main content:
|
apiPrefix={data.API_BASE_PREFIX}
|
||||||
<textarea
|
label="Main content"
|
||||||
name="main-content"
|
name="main-content"
|
||||||
rows="20"
|
content={data.article.main_content ?? ""}
|
||||||
bind:value={previewContent}
|
/>
|
||||||
bind:this={mainContentTextarea}
|
|
||||||
onscroll={updateScrollPercentage}
|
|
||||||
onpaste={handlePaste}
|
|
||||||
required>{data.article.main_content}</textarea
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import type { Actions, PageServerLoad } from "./$types";
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||||
import type { DocsCategory, DocsCategoryInput } from "$lib/db-schema";
|
import type { DocsCategory } from "$lib/db-schema";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => {
|
export const load: PageServerLoad = async ({ parent, params, fetch }) => {
|
||||||
const categoryData = await fetch(
|
const categories: DocsCategory[] = (
|
||||||
|
await apiRequest(
|
||||||
|
fetch,
|
||||||
`${API_BASE_PREFIX}/docs_category?website_id=eq.${params.websiteId}&order=category_weight.desc`,
|
`${API_BASE_PREFIX}/docs_category?website_id=eq.${params.websiteId}&order=category_weight.desc`,
|
||||||
|
"GET",
|
||||||
{
|
{
|
||||||
method: "GET",
|
returnData: true
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
);
|
).data;
|
||||||
|
|
||||||
const categories: DocsCategory[] = await categoryData.json();
|
|
||||||
const { website, home } = await parent();
|
const { website, home } = await parent();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -25,72 +24,44 @@ export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
createCategory: async ({ request, fetch, cookies, params }) => {
|
createCategory: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/docs_category`, {
|
return await apiRequest(fetch, `${API_BASE_PREFIX}/docs_category`, "POST", {
|
||||||
method: "POST",
|
body: {
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
website_id: params.websiteId,
|
website_id: params.websiteId,
|
||||||
category_name: data.get("category-name") as string,
|
category_name: data.get("category-name"),
|
||||||
category_weight: data.get("category-weight") as unknown as number
|
|
||||||
} satisfies DocsCategoryInput)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully created category" };
|
|
||||||
},
|
|
||||||
updateCategory: async ({ request, fetch, cookies, params }) => {
|
|
||||||
const data = await request.formData();
|
|
||||||
|
|
||||||
const res = await fetch(
|
|
||||||
`${API_BASE_PREFIX}/docs_category?website_id=eq.${params.websiteId}&id=eq.${data.get("category-id")}`,
|
|
||||||
{
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
category_weight: data.get("category-weight")
|
category_weight: data.get("category-weight")
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully updated category" };
|
|
||||||
},
|
},
|
||||||
deleteCategory: async ({ request, fetch, cookies, params }) => {
|
successMessage: "Successfully created category"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateCategory: async ({ request, fetch }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(
|
return await apiRequest(
|
||||||
`${API_BASE_PREFIX}/docs_category?website_id=eq.${params.websiteId}&id=eq.${data.get("category-id")}`,
|
fetch,
|
||||||
|
`${API_BASE_PREFIX}/docs_category?id=eq.${data.get("category-id")}`,
|
||||||
|
"PATCH",
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
body: {
|
||||||
headers: {
|
category_name: data.get("category-name"),
|
||||||
"Content-Type": "application/json",
|
category_weight: data.get("category-weight")
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
},
|
||||||
}
|
successMessage: "Successfully updated category"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
deleteCategory: async ({ request, fetch }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
|
||||||
if (!res.ok) {
|
return await apiRequest(
|
||||||
const response = await res.json();
|
fetch,
|
||||||
return { success: false, message: response.message };
|
`${API_BASE_PREFIX}/docs_category?id=eq.${data.get("category-id")}`,
|
||||||
|
"DELETE",
|
||||||
|
{
|
||||||
|
successMessage: "Successfully deleted category"
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return { success: true, message: "Successfully deleted category" };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,15 +5,18 @@
|
|||||||
import Modal from "$lib/components/Modal.svelte";
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
import type { ActionData, PageServerData } from "./$types";
|
import type { ActionData, PageServerData } from "./$types";
|
||||||
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
import { sending } from "$lib/runes.svelte";
|
||||||
|
import { previewContent } from "$lib/runes.svelte";
|
||||||
|
|
||||||
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||||
|
|
||||||
let sending = $state(false);
|
previewContent.value = data.home.main_content;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -21,7 +24,6 @@
|
|||||||
id={data.website.id}
|
id={data.website.id}
|
||||||
contentType={data.website.content_type}
|
contentType={data.website.content_type}
|
||||||
title={data.website.title}
|
title={data.website.title}
|
||||||
previewContent={data.home.main_content}
|
|
||||||
>
|
>
|
||||||
<section id="create-category">
|
<section id="create-category">
|
||||||
<h2>
|
<h2>
|
||||||
@@ -31,18 +33,7 @@
|
|||||||
<Modal id="create-category" text="Create category">
|
<Modal id="create-category" text="Create category">
|
||||||
<h3>Create category</h3>
|
<h3>Create category</h3>
|
||||||
|
|
||||||
<form
|
<form method="POST" action="?/createCategory" use:enhance={enhanceForm({ closeModal: true })}>
|
||||||
method="POST"
|
|
||||||
action="?/createCategory"
|
|
||||||
use:enhance={() => {
|
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label>
|
<label>
|
||||||
Name:
|
Name:
|
||||||
<input type="text" name="category-name" maxlength="50" required />
|
<input type="text" name="category-name" maxlength="50" required />
|
||||||
@@ -78,17 +69,21 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/updateCategory"
|
action="?/updateCategory"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ reset: false, closeModal: true })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update({ reset: false });
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input type="hidden" name="category-id" value={id} />
|
<input type="hidden" name="category-id" value={id} />
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Name:
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="category-name"
|
||||||
|
value={category_name}
|
||||||
|
maxlength="50"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Weight:
|
Weight:
|
||||||
<input type="number" name="category-weight" value={category_weight} min="0" />
|
<input type="number" name="category-weight" value={category_weight} min="0" />
|
||||||
@@ -105,14 +100,7 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/deleteCategory"
|
action="?/deleteCategory"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ closeModal: true })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input type="hidden" name="category-id" value={id} />
|
<input type="hidden" name="category-id" value={id} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import type { Actions, PageServerLoad } from "./$types";
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||||
import type { Collab, CollabInput, User } from "$lib/db-schema";
|
import type { Collab, User } from "$lib/db-schema";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ parent, params, fetch, cookies }) => {
|
export const load: PageServerLoad = async ({ parent, params, fetch }) => {
|
||||||
const { website, home } = await parent();
|
const collaborators: (Collab & { user: User })[] = (
|
||||||
|
await apiRequest(
|
||||||
const collabData = await fetch(
|
fetch,
|
||||||
`${API_BASE_PREFIX}/collab?website_id=eq.${params.websiteId}&select=*,user!user_id(*)&order=last_modified_at.desc,added_at.desc`,
|
`${API_BASE_PREFIX}/collab?website_id=eq.${params.websiteId}&select=*,user!user_id(*)&order=last_modified_at.desc,added_at.desc`,
|
||||||
|
"GET",
|
||||||
{
|
{
|
||||||
method: "GET",
|
returnData: true
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
);
|
).data;
|
||||||
|
|
||||||
const collaborators: (Collab & { user: User })[] = await collabData.json();
|
const { website, home } = await parent();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
website,
|
website,
|
||||||
@@ -26,83 +24,61 @@ export const load: PageServerLoad = async ({ parent, params, fetch, cookies }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
addCollaborator: async ({ request, fetch, cookies, params }) => {
|
addCollaborator: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const userData = await fetch(`${API_BASE_PREFIX}/user?username=eq.${data.get("username")}`, {
|
const user: User = (
|
||||||
method: "GET",
|
await apiRequest(
|
||||||
|
fetch,
|
||||||
|
`${API_BASE_PREFIX}/user?username=eq.${data.get("username")}`,
|
||||||
|
"GET",
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const user: User = await userData.json();
|
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/collab`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
returnData: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return { success: false, message: "Specified user could not be found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await apiRequest(fetch, `${API_BASE_PREFIX}/collab`, "POST", {
|
||||||
|
body: {
|
||||||
website_id: params.websiteId,
|
website_id: params.websiteId,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
permission_level: data.get("permission-level") as unknown as number
|
|
||||||
} satisfies CollabInput)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully added collaborator" };
|
|
||||||
},
|
|
||||||
updateCollaborator: async ({ request, fetch, cookies, params }) => {
|
|
||||||
const data = await request.formData();
|
|
||||||
|
|
||||||
const res = await fetch(
|
|
||||||
`${API_BASE_PREFIX}/collab?website_id=eq.${params.websiteId}&user_id=eq.${data.get("user-id")}`,
|
|
||||||
{
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
permission_level: data.get("permission-level")
|
permission_level: data.get("permission-level")
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Successfully updated collaborator" };
|
|
||||||
},
|
},
|
||||||
removeCollaborator: async ({ request, fetch, cookies, params }) => {
|
successMessage: "Successfully added collaborator"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateCollaborator: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(
|
return await apiRequest(
|
||||||
|
fetch,
|
||||||
`${API_BASE_PREFIX}/collab?website_id=eq.${params.websiteId}&user_id=eq.${data.get("user-id")}`,
|
`${API_BASE_PREFIX}/collab?website_id=eq.${params.websiteId}&user_id=eq.${data.get("user-id")}`,
|
||||||
|
"PATCH",
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
body: {
|
||||||
headers: {
|
permission_level: data.get("permission-level")
|
||||||
"Content-Type": "application/json",
|
},
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
successMessage: "Successfully updated collaborator"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
removeCollaborator: async ({ request, fetch, params }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
|
||||||
if (!res.ok) {
|
return await apiRequest(
|
||||||
const response = await res.json();
|
fetch,
|
||||||
return { success: false, message: response.message };
|
`${API_BASE_PREFIX}/collab?website_id=eq.${params.websiteId}&user_id=eq.${data.get("user-id")}`,
|
||||||
|
"DELETE",
|
||||||
|
{
|
||||||
|
successMessage: "Successfully removed collaborator"
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return { success: true, message: "Successfully removed collaborator" };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,16 +4,18 @@
|
|||||||
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
||||||
import Modal from "$lib/components/Modal.svelte";
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
import { previewContent, sending } from "$lib/runes.svelte";
|
||||||
import type { ActionData, PageServerData } from "./$types";
|
import type { ActionData, PageServerData } from "./$types";
|
||||||
|
|
||||||
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||||
|
|
||||||
let sending = $state(false);
|
previewContent.value = data.home.main_content;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -21,7 +23,6 @@
|
|||||||
id={data.website.id}
|
id={data.website.id}
|
||||||
contentType={data.website.content_type}
|
contentType={data.website.content_type}
|
||||||
title={data.website.title}
|
title={data.website.title}
|
||||||
previewContent={data.home.main_content}
|
|
||||||
>
|
>
|
||||||
<section id="add-collaborator">
|
<section id="add-collaborator">
|
||||||
<h2>
|
<h2>
|
||||||
@@ -34,14 +35,7 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/addCollaborator"
|
action="?/addCollaborator"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ closeModal: true })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<label>
|
<label>
|
||||||
Username:
|
Username:
|
||||||
@@ -82,14 +76,7 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/updateCollaborator"
|
action="?/updateCollaborator"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ reset: false, closeModal: true })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update({ reset: false });
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input type="hidden" name="user-id" value={user_id} />
|
<input type="hidden" name="user-id" value={user_id} />
|
||||||
|
|
||||||
@@ -113,14 +100,7 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/removeCollaborator"
|
action="?/removeCollaborator"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ closeModal: true })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input type="hidden" name="user-id" value={user_id} />
|
<input type="hidden" name="user-id" value={user_id} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +1,61 @@
|
|||||||
import type { Actions, PageServerLoad } from "./$types";
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||||
import { rm } from "node:fs/promises";
|
import { rm } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { LegalInformation, LegalInformationInput } from "$lib/db-schema";
|
import type { LegalInformation } from "$lib/db-schema";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ parent, fetch, params, cookies }) => {
|
export const load: PageServerLoad = async ({ parent, fetch, params }) => {
|
||||||
const legalInformationData = await fetch(
|
const legalInformation: LegalInformation = (
|
||||||
|
await apiRequest(
|
||||||
|
fetch,
|
||||||
`${API_BASE_PREFIX}/legal_information?website_id=eq.${params.websiteId}`,
|
`${API_BASE_PREFIX}/legal_information?website_id=eq.${params.websiteId}`,
|
||||||
|
"GET",
|
||||||
{
|
{
|
||||||
method: "GET",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
|
},
|
||||||
|
returnData: true
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
);
|
).data;
|
||||||
|
|
||||||
const legalInformation: LegalInformation = await legalInformationData.json();
|
|
||||||
const { website } = await parent();
|
const { website } = await parent();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
legalInformation,
|
legalInformation,
|
||||||
website
|
website,
|
||||||
|
API_BASE_PREFIX
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
createUpdateLegalInformation: async ({ request, fetch, cookies, params }) => {
|
createUpdateLegalInformation: async ({ request, fetch, params }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/legal_information`, {
|
return await apiRequest(fetch, `${API_BASE_PREFIX}/legal_information`, "POST", {
|
||||||
method: "POST",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Prefer: "resolution=merge-duplicates",
|
Prefer: "resolution=merge-duplicates",
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: {
|
||||||
website_id: params.websiteId,
|
website_id: params.websiteId,
|
||||||
main_content: data.get("main-content") as string
|
main_content: data.get("main-content")
|
||||||
} satisfies LegalInformationInput)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `Successfully ${res.status === 201 ? "created" : "updated"} legal information`
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
deleteLegalInformation: async ({ fetch, cookies, params }) => {
|
successMessage: "Successfully created/updated legal information"
|
||||||
const res = await fetch(
|
});
|
||||||
|
},
|
||||||
|
deleteLegalInformation: async ({ fetch, params }) => {
|
||||||
|
const deleteLegalInformation = await apiRequest(
|
||||||
|
fetch,
|
||||||
`${API_BASE_PREFIX}/legal_information?website_id=eq.${params.websiteId}`,
|
`${API_BASE_PREFIX}/legal_information?website_id=eq.${params.websiteId}`,
|
||||||
|
"DELETE",
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
successMessage: "Successfully deleted legal information"
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!deleteLegalInformation.success) {
|
||||||
const response = await res.json();
|
return deleteLegalInformation;
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await rm(
|
await rm(
|
||||||
@@ -76,6 +63,6 @@ export const actions: Actions = {
|
|||||||
{ force: true }
|
{ force: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { success: true, message: `Successfully deleted legal information` };
|
return deleteLegalInformation;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,25 +4,19 @@
|
|||||||
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
import Modal from "$lib/components/Modal.svelte";
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
import { sending, previewContent } from "$lib/runes.svelte";
|
||||||
import type { ActionData, PageServerData } from "./$types";
|
import type { ActionData, PageServerData } from "./$types";
|
||||||
|
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
|
||||||
|
|
||||||
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||||
|
|
||||||
let previewContent = $state(data.legalInformation.main_content);
|
previewContent.value = data.legalInformation?.main_content ?? "";
|
||||||
let mainContentTextarea: HTMLTextAreaElement;
|
|
||||||
let textareaScrollTop = $state(0);
|
|
||||||
|
|
||||||
const updateScrollPercentage = () => {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = mainContentTextarea;
|
|
||||||
textareaScrollTop = (scrollTop / (scrollHeight - clientHeight)) * 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
let sending = $state(false);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -30,9 +24,6 @@
|
|||||||
id={data.website.id}
|
id={data.website.id}
|
||||||
contentType={data.website.content_type}
|
contentType={data.website.content_type}
|
||||||
title={data.website.title}
|
title={data.website.title}
|
||||||
previewContent={previewContent ||
|
|
||||||
"Put some markdown content in main content to see a live preview here"}
|
|
||||||
previewScrollTop={textareaScrollTop}
|
|
||||||
>
|
>
|
||||||
<section id="legal-information">
|
<section id="legal-information">
|
||||||
<h2>
|
<h2>
|
||||||
@@ -61,45 +52,24 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/createUpdateLegalInformation"
|
action="?/createUpdateLegalInformation"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ reset: false })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update({ reset: false });
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<label>
|
<MarkdownEditor
|
||||||
Main content:
|
apiPrefix={data.API_BASE_PREFIX}
|
||||||
<textarea
|
label="Main content"
|
||||||
name="main-content"
|
name="main-content"
|
||||||
rows="20"
|
content={data.legalInformation?.main_content ?? ""}
|
||||||
placeholder="## Impressum
|
/>
|
||||||
|
|
||||||
## Privacy policy"
|
|
||||||
bind:value={previewContent}
|
|
||||||
bind:this={mainContentTextarea}
|
|
||||||
onscroll={updateScrollPercentage}
|
|
||||||
required>{data.legalInformation.main_content ?? ""}</textarea
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{#if data.legalInformation.main_content}
|
{#if data.legalInformation?.main_content}
|
||||||
<Modal id="delete-legal-information" text="Delete">
|
<Modal id="delete-legal-information" text="Delete">
|
||||||
<form
|
<form
|
||||||
action="?/deleteLegalInformation"
|
action="?/deleteLegalInformation"
|
||||||
method="post"
|
method="post"
|
||||||
use:enhance={() => {
|
use:enhance={enhanceForm({ closeModal: true })}
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
window.location.hash = "!";
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<h3>Delete legal information</h3>
|
<h3>Delete legal information</h3>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
import type { PageServerLoad } from "./$types";
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||||
import type { ChangeLog, User, Collab } from "$lib/db-schema";
|
import type { ChangeLog, User, Collab } from "$lib/db-schema";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ parent, fetch, params, cookies, url }) => {
|
export const load: PageServerLoad = async ({ parent, fetch, params, url }) => {
|
||||||
const userFilter = url.searchParams.get("logs_filter_user");
|
const userFilter = url.searchParams.get("logs_filter_user");
|
||||||
const resourceFilter = url.searchParams.get("logs_filter_resource");
|
const resourceFilter = url.searchParams.get("logs_filter_resource");
|
||||||
const operationFilter = url.searchParams.get("logs_filter_operation");
|
const operationFilter = url.searchParams.get("logs_filter_operation");
|
||||||
@@ -27,41 +27,30 @@ export const load: PageServerLoad = async ({ parent, fetch, params, cookies, url
|
|||||||
|
|
||||||
const constructedFetchUrl = `${baseFetchUrl}&${searchParams.toString()}&limit=50&offset=${resultOffset}`;
|
const constructedFetchUrl = `${baseFetchUrl}&${searchParams.toString()}&limit=50&offset=${resultOffset}`;
|
||||||
|
|
||||||
const changeLogData = await fetch(constructedFetchUrl, {
|
const changeLog: (ChangeLog & { user: { username: User["username"] } })[] = (
|
||||||
method: "GET",
|
await apiRequest(fetch, constructedFetchUrl, "GET", { returnData: true })
|
||||||
headers: {
|
).data;
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const resultChangeLogData = await fetch(constructedFetchUrl, {
|
const resultChangeLogData = await apiRequest(fetch, constructedFetchUrl, "HEAD", {
|
||||||
method: "HEAD",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Prefer: "count=exact"
|
Prefer: "count=exact"
|
||||||
}
|
},
|
||||||
|
returnData: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const resultChangeLogCount = Number(
|
const resultChangeLogCount = Number(
|
||||||
resultChangeLogData.headers.get("content-range")?.split("/").at(-1)
|
resultChangeLogData.data.headers.get("content-range")?.split("/").at(-1)
|
||||||
);
|
);
|
||||||
|
|
||||||
const collabData = await fetch(
|
const collaborators: (Collab & { user: User })[] = (
|
||||||
|
await apiRequest(
|
||||||
|
fetch,
|
||||||
`${API_BASE_PREFIX}/collab?website_id=eq.${params.websiteId}&select=*,user!user_id(*)&order=last_modified_at.desc,added_at.desc`,
|
`${API_BASE_PREFIX}/collab?website_id=eq.${params.websiteId}&select=*,user!user_id(*)&order=last_modified_at.desc,added_at.desc`,
|
||||||
{
|
"GET",
|
||||||
method: "GET",
|
{ returnData: true }
|
||||||
headers: {
|
)
|
||||||
"Content-Type": "application/json",
|
).data;
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeLog: (ChangeLog & { user: { username: User["username"] } })[] =
|
|
||||||
await changeLogData.json();
|
|
||||||
const collaborators: (Collab & { user: User })[] = await collabData.json();
|
|
||||||
const { website, home } = await parent();
|
const { website, home } = await parent();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
import diff from "fast-diff";
|
import diff from "fast-diff";
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import { tables } from "$lib/db-schema";
|
import { tables } from "$lib/db-schema";
|
||||||
|
import { previewContent } from "$lib/runes.svelte";
|
||||||
|
import { sanitize } from "isomorphic-dompurify";
|
||||||
|
|
||||||
const { data }: { data: PageServerData } = $props();
|
const { data }: { data: PageServerData } = $props();
|
||||||
|
|
||||||
@@ -34,15 +36,19 @@
|
|||||||
let resources = $state({});
|
let resources = $state({});
|
||||||
|
|
||||||
if (data.website.content_type === "Blog") {
|
if (data.website.content_type === "Blog") {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { user, change_log, media, docs_category, ...restTables } = tables;
|
const { user, change_log, media, docs_category, ...restTables } = tables;
|
||||||
resources = restTables;
|
resources = restTables;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.website.content_type === "Docs") {
|
if (data.website.content_type === "Docs") {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { user, change_log, media, ...restTables } = tables;
|
const { user, change_log, media, ...restTables } = tables;
|
||||||
resources = restTables;
|
resources = restTables;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previewContent.value = data.home.main_content;
|
||||||
|
|
||||||
let logsSection: HTMLElement;
|
let logsSection: HTMLElement;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -50,7 +56,6 @@
|
|||||||
id={data.website.id}
|
id={data.website.id}
|
||||||
contentType={data.website.content_type}
|
contentType={data.website.content_type}
|
||||||
title={data.website.title}
|
title={data.website.title}
|
||||||
previewContent={data.home.main_content}
|
|
||||||
>
|
>
|
||||||
<section id="logs" bind:this={logsSection}>
|
<section id="logs" bind:this={logsSection}>
|
||||||
<hgroup>
|
<hgroup>
|
||||||
@@ -151,13 +156,17 @@
|
|||||||
<p>{table_name} — {operation}</p>
|
<p>{table_name} — {operation}</p>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
|
|
||||||
<pre style="white-space: pre-wrap">{@html htmlDiff(oldValue, newValue)}</pre>
|
<pre style="white-space: pre-wrap">{@html sanitize(htmlDiff(oldValue, newValue), {
|
||||||
|
ALLOWED_TAGS: ["ins", "del"]
|
||||||
|
})}</pre>
|
||||||
</Modal>
|
</Modal>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="pagination">
|
||||||
{#snippet commonFilterInputs()}
|
{#snippet commonFilterInputs()}
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@@ -175,7 +184,6 @@
|
|||||||
value={$page.url.searchParams.get("logs_filter_operation")}
|
value={$page.url.searchParams.get("logs_filter_operation")}
|
||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<div class="pagination">
|
|
||||||
<p>
|
<p>
|
||||||
{$page.url.searchParams.get("logs_results_page") ?? 1} / {Math.max(
|
{$page.url.searchParams.get("logs_results_page") ?? 1} / {Math.max(
|
||||||
Math.ceil(data.resultChangeLogCount / 50),
|
Math.ceil(data.resultChangeLogCount / 50),
|
||||||
@@ -187,8 +195,7 @@
|
|||||||
{@render commonFilterInputs()}
|
{@render commonFilterInputs()}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={($page.url.searchParams.get("logs_results_page") ?? "1") === "1"}
|
disabled={($page.url.searchParams.get("logs_results_page") ?? "1") === "1"}>First</button
|
||||||
>First</button
|
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
<form method="GET">
|
<form method="GET">
|
||||||
@@ -237,7 +244,6 @@
|
|||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</WebsiteEditor>
|
</WebsiteEditor>
|
||||||
|
|
||||||
@@ -245,8 +251,6 @@
|
|||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-inline: var(--space-2xs);
|
|
||||||
margin-block: var(--space-s);
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
@@ -256,8 +260,8 @@
|
|||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled] {
|
button:disabled {
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
color: hsl(0 0% 50%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { type WebsiteOverview, slugify } from "$lib/utils";
|
|
||||||
import type { Actions, PageServerLoad } from "./$types";
|
|
||||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
|
||||||
import { render } from "svelte/server";
|
|
||||||
import BlogIndex from "$lib/templates/blog/BlogIndex.svelte";
|
|
||||||
import BlogArticle from "$lib/templates/blog/BlogArticle.svelte";
|
|
||||||
import DocsIndex from "$lib/templates/docs/DocsIndex.svelte";
|
|
||||||
import DocsArticle from "$lib/templates/docs/DocsArticle.svelte";
|
|
||||||
import { dev } from "$app/environment";
|
import { dev } from "$app/environment";
|
||||||
|
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
|
||||||
|
import BlogArticle from "$lib/templates/blog/BlogArticle.svelte";
|
||||||
|
import BlogIndex from "$lib/templates/blog/BlogIndex.svelte";
|
||||||
|
import DocsArticle from "$lib/templates/docs/DocsArticle.svelte";
|
||||||
|
import DocsIndex from "$lib/templates/docs/DocsIndex.svelte";
|
||||||
|
import { type WebsiteOverview, hexToHSL, slugify } from "$lib/utils";
|
||||||
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { render } from "svelte/server";
|
||||||
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
|
export const load: PageServerLoad = async ({ params, fetch }) => {
|
||||||
const websiteOverviewData = await fetch(
|
const websiteOverview: WebsiteOverview = (
|
||||||
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*)`,
|
await apiRequest(
|
||||||
|
fetch,
|
||||||
|
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*),domain_prefix(*)`,
|
||||||
|
"GET",
|
||||||
{
|
{
|
||||||
method: "GET",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
|
},
|
||||||
|
returnData: true
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
);
|
).data;
|
||||||
|
|
||||||
const websiteOverview: WebsiteOverview = await websiteOverviewData.json();
|
|
||||||
|
|
||||||
generateStaticFiles(websiteOverview);
|
generateStaticFiles(websiteOverview);
|
||||||
|
|
||||||
@@ -36,10 +36,13 @@ export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
|
|||||||
}/previews/${websiteOverview.id}/`;
|
}/previews/${websiteOverview.id}/`;
|
||||||
|
|
||||||
const websiteProdUrl = dev
|
const websiteProdUrl = dev
|
||||||
? `http://localhost:18000/${websiteOverview.id}/`
|
? `http://localhost:18000/${websiteOverview.domain_prefix?.prefix ?? websiteOverview.id}/`
|
||||||
: process.env.ORIGIN
|
: process.env.ORIGIN
|
||||||
? process.env.ORIGIN.replace("//", `//${websiteOverview.id}.`)
|
? process.env.ORIGIN.replace(
|
||||||
: `http://localhost:18000/${websiteOverview.id}/`;
|
"//",
|
||||||
|
`//${websiteOverview.domain_prefix?.prefix ?? websiteOverview.id}.`
|
||||||
|
)
|
||||||
|
: `http://localhost:18000/${websiteOverview.domain_prefix?.prefix ?? websiteOverview.id}/`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
websiteOverview,
|
websiteOverview,
|
||||||
@@ -49,43 +52,110 @@ export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
publishWebsite: async ({ fetch, params, cookies }) => {
|
publishWebsite: async ({ fetch, params }) => {
|
||||||
const websiteOverviewData = await fetch(
|
const websiteOverview: WebsiteOverview = (
|
||||||
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*)`,
|
await apiRequest(
|
||||||
|
fetch,
|
||||||
|
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*),domain_prefix(*)`,
|
||||||
|
"GET",
|
||||||
{
|
{
|
||||||
method: "GET",
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`,
|
|
||||||
Accept: "application/vnd.pgrst.object+json"
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
|
},
|
||||||
|
returnData: true
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
generateStaticFiles(websiteOverview, false);
|
||||||
|
|
||||||
|
return await apiRequest(
|
||||||
|
fetch,
|
||||||
|
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}`,
|
||||||
|
"PATCH",
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
is_published: true
|
||||||
|
},
|
||||||
|
successMessage: "Successfully published website"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
createUpdateCustomDomainPrefix: async ({ request, fetch, params }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
|
||||||
|
const oldDomainPrefix = (
|
||||||
|
await apiRequest(
|
||||||
|
fetch,
|
||||||
|
`${API_BASE_PREFIX}/domain_prefix?website_id=eq.${params.websiteId}`,
|
||||||
|
"GET",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
|
},
|
||||||
|
returnData: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
const newDomainPrefix = await apiRequest(fetch, `${API_BASE_PREFIX}/domain_prefix`, "POST", {
|
||||||
|
headers: {
|
||||||
|
Prefer: "resolution=merge-duplicates",
|
||||||
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
website_id: params.websiteId,
|
||||||
|
prefix: data.get("domain-prefix")
|
||||||
|
},
|
||||||
|
successMessage: "Successfully created/updated domain prefix"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newDomainPrefix.success) {
|
||||||
|
return newDomainPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
await rename(
|
||||||
|
join(
|
||||||
|
"/",
|
||||||
|
"var",
|
||||||
|
"www",
|
||||||
|
"archtika-websites",
|
||||||
|
oldDomainPrefix?.prefix ? oldDomainPrefix.prefix : params.websiteId
|
||||||
|
),
|
||||||
|
join("/", "var", "www", "archtika-websites", data.get("domain-prefix") as string)
|
||||||
|
);
|
||||||
|
|
||||||
|
return newDomainPrefix;
|
||||||
|
},
|
||||||
|
deleteCustomDomainPrefix: async ({ fetch, params }) => {
|
||||||
|
const customPrefix = await apiRequest(
|
||||||
|
fetch,
|
||||||
|
`${API_BASE_PREFIX}/domain_prefix?website_id=eq.${params.websiteId}`,
|
||||||
|
"DELETE",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Prefer: "return=representation",
|
||||||
|
Accept: "application/vnd.pgrst.object+json"
|
||||||
|
},
|
||||||
|
successMessage: "Successfully deleted domain prefix",
|
||||||
|
returnData: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const websiteOverview = await websiteOverviewData.json();
|
if (!customPrefix.success) {
|
||||||
generateStaticFiles(websiteOverview, false);
|
return customPrefix;
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
is_published: true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const response = await res.json();
|
|
||||||
return { success: false, message: response.message };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, message: "Successfully published website" };
|
await rename(
|
||||||
|
join("/", "var", "www", "archtika-websites", customPrefix.data.prefix),
|
||||||
|
join("/", "var", "www", "archtika-websites", params.websiteId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return customPrefix;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview: boolean = true) => {
|
const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview = true) => {
|
||||||
const fileContents = (head: string, body: string) => {
|
const fileContents = (head: string, body: string) => {
|
||||||
return `
|
return `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -112,7 +182,13 @@ const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview: bool
|
|||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
uploadDir = join("/", "var", "www", "archtika-websites", "previews", websiteData.id);
|
uploadDir = join("/", "var", "www", "archtika-websites", "previews", websiteData.id);
|
||||||
} else {
|
} else {
|
||||||
uploadDir = join("/", "var", "www", "archtika-websites", websiteData.id);
|
uploadDir = join(
|
||||||
|
"/",
|
||||||
|
"var",
|
||||||
|
"www",
|
||||||
|
"archtika-websites",
|
||||||
|
websiteData.domain_prefix?.prefix ?? websiteData.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await mkdir(uploadDir, { recursive: true });
|
await mkdir(uploadDir, { recursive: true });
|
||||||
@@ -157,21 +233,35 @@ const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview: bool
|
|||||||
encoding: "utf-8"
|
encoding: "utf-8"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
h: hDark,
|
||||||
|
s: sDark,
|
||||||
|
l: lDark
|
||||||
|
} = hexToHSL(websiteData.settings.background_color_dark_theme);
|
||||||
|
const {
|
||||||
|
h: hLight,
|
||||||
|
s: sLight,
|
||||||
|
l: lLight
|
||||||
|
} = hexToHSL(websiteData.settings.background_color_light_theme);
|
||||||
|
|
||||||
await writeFile(
|
await writeFile(
|
||||||
join(uploadDir, "styles.css"),
|
join(uploadDir, "styles.css"),
|
||||||
commonStyles
|
commonStyles
|
||||||
.concat(specificStyles)
|
.concat(specificStyles)
|
||||||
|
.replace(/(?<=\/\* BACKGROUND_COLOR_DARK_THEME_H \*\/\s*).*(?=;)/, ` ${hDark}`)
|
||||||
|
.replace(/(?<=\/\* BACKGROUND_COLOR_DARK_THEME_S \*\/\s*).*(?=;)/, ` ${sDark}%`)
|
||||||
|
.replace(/(?<=\/\* BACKGROUND_COLOR_DARK_THEME_L \*\/\s*).*(?=;)/, ` ${lDark}%`)
|
||||||
|
.replace(/(?<=\/\* BACKGROUND_COLOR_LIGHT_THEME_H \*\/\s*).*(?=;)/, ` ${hLight}`)
|
||||||
|
.replace(/(?<=\/\* BACKGROUND_COLOR_LIGHT_THEME_S \*\/\s*).*(?=;)/, ` ${sLight}%`)
|
||||||
|
.replace(/(?<=\/\* BACKGROUND_COLOR_LIGHT_THEME_L \*\/\s*).*(?=;)/, ` ${lLight}%`)
|
||||||
.replace(
|
.replace(
|
||||||
/--color-accent:\s*(.*?);/,
|
/(?<=\/\* ACCENT_COLOR_DARK_THEME \*\/\s*).*(?=;)/,
|
||||||
`--color-accent: ${websiteData.settings.accent_color_dark_theme};`
|
` ${websiteData.settings.accent_color_dark_theme}`
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
/@media\s*\(prefers-color-scheme:\s*dark\)\s*{[^}]*--color-accent:\s*(.*?);/,
|
/(?<=\/\* ACCENT_COLOR_LIGHT_THEME \*\/\s*).*(?=;)/,
|
||||||
(match) =>
|
` ${websiteData.settings.accent_color_light_theme}`
|
||||||
match.replace(
|
|
||||||
/--color-accent:\s*(.*?);/,
|
|
||||||
`--color-accent: ${websiteData.settings.accent_color_light_theme};`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,15 +4,19 @@
|
|||||||
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
|
||||||
import type { ActionData, PageServerData } from "./$types";
|
import type { ActionData, PageServerData } from "./$types";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
|
import { enhanceForm } from "$lib/utils";
|
||||||
|
import { sending } from "$lib/runes.svelte";
|
||||||
|
import { previewContent } from "$lib/runes.svelte";
|
||||||
|
|
||||||
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||||
|
|
||||||
let sending = $state(false);
|
previewContent.value = data.websitePreviewUrl;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SuccessOrError success={form?.success} message={form?.message} />
|
<SuccessOrError success={form?.success} message={form?.message} />
|
||||||
|
|
||||||
{#if sending}
|
{#if sending.value}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -20,7 +24,6 @@
|
|||||||
id={data.websiteOverview.id}
|
id={data.websiteOverview.id}
|
||||||
contentType={data.websiteOverview.content_type}
|
contentType={data.websiteOverview.content_type}
|
||||||
title={data.websiteOverview.title}
|
title={data.websiteOverview.title}
|
||||||
previewContent={data.websitePreviewUrl}
|
|
||||||
fullPreview={true}
|
fullPreview={true}
|
||||||
>
|
>
|
||||||
<section id="publish-website">
|
<section id="publish-website">
|
||||||
@@ -32,31 +35,63 @@
|
|||||||
is published. If you are happy with the results, click the button below and your website will
|
is published. If you are happy with the results, click the button below and your website will
|
||||||
be published on the Internet.
|
be published on the Internet.
|
||||||
</p>
|
</p>
|
||||||
<form
|
<form method="POST" action="?/publishWebsite" use:enhance={enhanceForm()}>
|
||||||
method="POST"
|
|
||||||
action="?/publishWebsite"
|
|
||||||
use:enhance={() => {
|
|
||||||
sending = true;
|
|
||||||
return async ({ update }) => {
|
|
||||||
await update();
|
|
||||||
sending = false;
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button type="submit">Publish</button>
|
<button type="submit">Publish</button>
|
||||||
</form>
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
{#if data.websiteOverview.is_published}
|
{#if data.websiteOverview.is_published}
|
||||||
<section id="publication-status">
|
<section id="publication-status">
|
||||||
<h3>
|
<h2>
|
||||||
<a href="#publication-status">Publication status</a>
|
<a href="#publication-status">Publication status</a>
|
||||||
</h3>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Your website is published at:
|
Your website is published at:
|
||||||
<br />
|
<br />
|
||||||
<a href={data.websiteProdUrl}>{data.websiteProdUrl}</a>
|
<a href={data.websiteProdUrl}>{data.websiteProdUrl}</a>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="custom-domain-prefix">
|
||||||
|
<h2>
|
||||||
|
<a href="#custom-domain-prefix">Custom domain prefix</a>
|
||||||
|
</h2>
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="?/createUpdateCustomDomainPrefix"
|
||||||
|
use:enhance={enhanceForm({ reset: false })}
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
Prefix:
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="domain-prefix"
|
||||||
|
value={data.websiteOverview.domain_prefix?.prefix ?? ""}
|
||||||
|
placeholder="my-blog"
|
||||||
|
minlength="3"
|
||||||
|
maxlength="16"
|
||||||
|
pattern="^[a-z]+(-[a-z]+)*$"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
{#if data.websiteOverview.domain_prefix?.prefix}
|
||||||
|
<Modal id="delete-domain-prefix" text="Delete">
|
||||||
|
<form
|
||||||
|
action="?/deleteCustomDomainPrefix"
|
||||||
|
method="post"
|
||||||
|
use:enhance={enhanceForm({ closeModal: true })}
|
||||||
|
>
|
||||||
|
<h3>Delete domain prefix</h3>
|
||||||
|
<p>
|
||||||
|
<strong>Caution!</strong>
|
||||||
|
This action will remove the domain prefix and reset it to its initial value.
|
||||||
|
</p>
|
||||||
|
<button type="submit">Delete domain prefix</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
{/if}
|
||||||
</WebsiteEditor>
|
</WebsiteEditor>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import { navigating } from "$app/stores";
|
import { navigating } from "$app/stores";
|
||||||
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
|
||||||
|
import { LOADING_DELAY } from "$lib/utils";
|
||||||
|
|
||||||
const { data, children }: { data: LayoutServerData; children: Snippet } = $props();
|
const { data, children }: { data: LayoutServerData; children: Snippet } = $props();
|
||||||
|
|
||||||
@@ -14,23 +15,43 @@
|
|||||||
? "Dashboard"
|
? "Dashboard"
|
||||||
: `${$page.url.pathname.charAt(1).toUpperCase()}${$page.url.pathname.slice(2)}`
|
: `${$page.url.pathname.charAt(1).toUpperCase()}${$page.url.pathname.slice(2)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let loading = $state(false);
|
||||||
|
let loadingDelay: number;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($navigating && ["link", "goto"].includes($navigating.type)) {
|
||||||
|
loadingDelay = window.setTimeout(() => (loading = true), LOADING_DELAY);
|
||||||
|
} else {
|
||||||
|
window.clearTimeout(loadingDelay);
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $navigating && ["link", "goto"].includes($navigating.type)}
|
{#if loading}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>archtika | {routeName.replaceAll("/", " - ")}</title>
|
<title>archtika | {routeName.replaceAll("/", " - ")}</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="FLOSS, modern, performant and lightweight CMS (Content Mangement System) with predefined templates"
|
||||||
|
/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
|
{#if data.user}
|
||||||
|
<div class="logo-wrapper">
|
||||||
<img src="/favicon.svg" width="24" height="24" alt="" />
|
<img src="/favicon.svg" width="24" height="24" alt="" />
|
||||||
|
<a href="/">archtika</a>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<img src="/favicon.svg" width="24" height="24" alt="" />
|
||||||
|
{/if}
|
||||||
<ul class="link-wrapper unpadded">
|
<ul class="link-wrapper unpadded">
|
||||||
{#if data.user}
|
{#if data.user}
|
||||||
<li>
|
|
||||||
<a href="/">Dashboard</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<a href="/account">Account</a>
|
<a href="/account">Account</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -83,6 +104,12 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav > .logo-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
nav > .link-wrapper {
|
nav > .link-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -11,7 +11,23 @@ const config = {
|
|||||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
adapter: adapter()
|
adapter: adapter(),
|
||||||
|
csp: {
|
||||||
|
mode: "auto",
|
||||||
|
directives: {
|
||||||
|
"default-src": ["none"],
|
||||||
|
"script-src": ["self"],
|
||||||
|
"style-src": ["self", "https:", "unsafe-inline"],
|
||||||
|
"img-src": ["self", "data:", "https:", "http:"],
|
||||||
|
"font-src": ["self", "https:"],
|
||||||
|
"connect-src": ["self"],
|
||||||
|
"frame-src": ["self", "https:", "http:"],
|
||||||
|
"object-src": ["none"],
|
||||||
|
"base-uri": ["self"],
|
||||||
|
"frame-ancestors": ["none"],
|
||||||
|
"form-action": ["self"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,16 +19,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: white;
|
--bg-primary-h: /* BACKGROUND_COLOR_LIGHT_THEME_H */ 0;
|
||||||
--bg-secondary: hsl(0 0% 95%);
|
--bg-primary-s: /* BACKGROUND_COLOR_LIGHT_THEME_S */ 0%;
|
||||||
--bg-tertiary: hsl(0 0% 90%);
|
--bg-primary-l: /* BACKGROUND_COLOR_LIGHT_THEME_L */ 100%;
|
||||||
|
--bg-primary: hsl(var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l));
|
||||||
|
--bg-secondary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 5%));
|
||||||
|
--bg-tertiary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 10%));
|
||||||
|
--bg-blurred: hsla(
|
||||||
|
var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l) / calc(var(--bg-primary-l) - 20%)
|
||||||
|
);
|
||||||
|
|
||||||
--color-text: black;
|
--color-text: hsl(var(--bg-primary-h) var(--bg-primary-s) 0%);
|
||||||
--color-text-invert: white;
|
--color-text-invert: var(--bg-primary);
|
||||||
--color-border: hsl(0 0% 50%);
|
--color-border: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 50%));
|
||||||
--color-accent: hsl(210, 100%, 30%);
|
--color-accent: /* ACCENT_COLOR_LIGHT_THEME */ hsl(210 100% 30%);
|
||||||
--color-success: hsl(105, 100%, 30%);
|
--color-success: hsl(105 100% 30%);
|
||||||
--color-error: hsl(0, 100%, 30%);
|
--color-error: hsl(0 100% 30%);
|
||||||
|
|
||||||
--border-primary: 0.0625rem solid var(--color-border);
|
--border-primary: 0.0625rem solid var(--color-border);
|
||||||
--border-radius: 0.125rem;
|
--border-radius: 0.125rem;
|
||||||
@@ -72,15 +78,22 @@
|
|||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: hsl(0 0% 15%);
|
--bg-primary-h: /* BACKGROUND_COLOR_DARK_THEME_H */ 0;
|
||||||
--bg-secondary: hsl(0 0% 20%);
|
--bg-primary-s: /* BACKGROUND_COLOR_DARK_THEME_S */ 0%;
|
||||||
--bg-tertiary: hsl(0 0% 25%);
|
--bg-primary-l: /* BACKGROUND_COLOR_DARK_THEME_L */ 15%;
|
||||||
|
--bg-primary: hsl(var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l));
|
||||||
|
--bg-secondary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) + 5%));
|
||||||
|
--bg-tertiary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) + 10%));
|
||||||
|
--bg-blurred: hsla(
|
||||||
|
var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l) / calc(var(--bg-primary-l) + 20%)
|
||||||
|
);
|
||||||
|
|
||||||
--color-text: white;
|
--color-text: hsl(var(--bg-primary-h) var(--bg-primary-s) 100%);
|
||||||
--color-text-invert: black;
|
--color-text-invert: var(--bg-primary);
|
||||||
--color-accent: hsl(210, 100%, 80%);
|
--color-border: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) + 50%));
|
||||||
--color-success: hsl(105, 100%, 80%);
|
--color-accent: /* ACCENT_COLOR_DARK_THEME */ hsl(210 100% 80%);
|
||||||
--color-error: hsl(0, 100%, 80%);
|
--color-success: hsl(105 100% 80%);
|
||||||
|
--color-error: hsl(0 100% 80%);
|
||||||
|
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,8 +86,8 @@ test.describe.serial("Collaborator tests", () => {
|
|||||||
await page.getByRole("link", { name: "Documentation" }).click();
|
await page.getByRole("link", { name: "Documentation" }).click();
|
||||||
await page.getByRole("link", { name: "Categories" }).click();
|
await page.getByRole("link", { name: "Categories" }).click();
|
||||||
await page.getByRole("button", { name: "Create category" }).click();
|
await page.getByRole("button", { name: "Create category" }).click();
|
||||||
await page.getByLabel("Name:").click();
|
await page.getByLabel("Name:").nth(0).click();
|
||||||
await page.getByLabel("Name:").fill("Category-10");
|
await page.getByLabel("Name:").nth(0).fill("Category-10");
|
||||||
await page.getByLabel("Weight:").click();
|
await page.getByLabel("Weight:").click();
|
||||||
await page.getByLabel("Weight:").fill("10");
|
await page.getByLabel("Weight:").fill("10");
|
||||||
await page.getByRole("button", { name: "Submit" }).click();
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
@@ -99,8 +99,8 @@ test.describe.serial("Collaborator tests", () => {
|
|||||||
await page.getByRole("button", { name: "Submit" }).click();
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
|
|
||||||
await page.getByRole("link", { name: "Legal information" }).click();
|
await page.getByRole("link", { name: "Legal information" }).click();
|
||||||
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click();
|
await page.getByLabel("Main content:").click();
|
||||||
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content");
|
await page.getByLabel("Main content:").fill("## Content");
|
||||||
await page.getByRole("button", { name: "Submit" }).click();
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -330,22 +330,22 @@ test.describe.serial("Collaborator tests", () => {
|
|||||||
test("Create/Update legal information", async ({ page }) => {
|
test("Create/Update legal information", async ({ page }) => {
|
||||||
await page.getByRole("link", { name: "Blog" }).click();
|
await page.getByRole("link", { name: "Blog" }).click();
|
||||||
await page.getByRole("link", { name: "Legal information" }).click();
|
await page.getByRole("link", { name: "Legal information" }).click();
|
||||||
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click();
|
await page.getByLabel("Main content:").click();
|
||||||
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content");
|
await page.getByLabel("Main content:").fill("## Content");
|
||||||
await page.getByRole("button", { name: "Submit" }).click();
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
|
|
||||||
if (permissionLevel === 30) {
|
if (permissionLevel === 30) {
|
||||||
await expect(page.getByText("Successfully created legal")).toBeVisible();
|
await expect(page.getByText("Successfully created/updated legal")).toBeVisible();
|
||||||
} else {
|
} else {
|
||||||
await expect(page.getByText("Insufficient permissions")).toBeVisible();
|
await expect(page.getByText("Insufficient permissions")).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click();
|
await page.getByLabel("Main content:").click();
|
||||||
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content updated");
|
await page.getByLabel("Main content:").fill("## Content updated");
|
||||||
await page.getByRole("button", { name: "Submit" }).click();
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
|
|
||||||
if (permissionLevel === 30) {
|
if (permissionLevel === 30) {
|
||||||
await expect(page.getByText("Successfully updated legal")).toBeVisible();
|
await expect(page.getByText("Successfully created/updated legal")).toBeVisible();
|
||||||
} else {
|
} else {
|
||||||
await expect(page.getByText("Insufficient permissions")).toBeVisible();
|
await expect(page.getByText("Insufficient permissions")).toBeVisible();
|
||||||
}
|
}
|
||||||
@@ -370,8 +370,8 @@ test.describe.serial("Collaborator tests", () => {
|
|||||||
await page.getByRole("link", { name: "Documentation" }).click();
|
await page.getByRole("link", { name: "Documentation" }).click();
|
||||||
await page.getByRole("link", { name: "Categories" }).click();
|
await page.getByRole("link", { name: "Categories" }).click();
|
||||||
await page.getByRole("button", { name: "Create category" }).click();
|
await page.getByRole("button", { name: "Create category" }).click();
|
||||||
await page.getByLabel("Name:").click();
|
await page.getByLabel("Name:").nth(0).click();
|
||||||
await page.getByLabel("Name:").fill(`Category-${permissionLevel}`);
|
await page.getByLabel("Name:").nth(0).fill(`Category-${permissionLevel}`);
|
||||||
await page.getByRole("spinbutton", { name: "Weight:" }).click();
|
await page.getByRole("spinbutton", { name: "Weight:" }).click();
|
||||||
await page.getByRole("spinbutton", { name: "Weight:" }).fill(permissionLevel.toString());
|
await page.getByRole("spinbutton", { name: "Weight:" }).fill(permissionLevel.toString());
|
||||||
await page.getByRole("button", { name: "Submit" }).click();
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
|
|||||||
@@ -80,10 +80,14 @@ test.describe.serial("Website tests", () => {
|
|||||||
test.describe.serial("Update settings", () => {
|
test.describe.serial("Update settings", () => {
|
||||||
test("Global", async ({ authenticatedPage: page }) => {
|
test("Global", async ({ authenticatedPage: page }) => {
|
||||||
await page.getByRole("link", { name: "Blog" }).click();
|
await page.getByRole("link", { name: "Blog" }).click();
|
||||||
await page.getByLabel("Light accent color:").click();
|
await page.getByLabel("Background color dark theme: ").click();
|
||||||
await page.getByLabel("Light accent color:").fill("#3975a2");
|
await page.getByLabel("Background color dark theme:").fill("#3975a2");
|
||||||
await page.getByLabel("Dark accent color:").click();
|
await page.getByLabel("Background color light theme:").click();
|
||||||
await page.getByLabel("Dark accent color:").fill("#41473e");
|
await page.getByLabel("Background color light theme:").fill("#41473e");
|
||||||
|
await page.getByLabel("Accent color dark theme: ").click();
|
||||||
|
await page.getByLabel("Accent color dark theme:").fill("#3975a2");
|
||||||
|
await page.getByLabel("Accent color light theme:").click();
|
||||||
|
await page.getByLabel("Accent color light theme:").fill("#41473e");
|
||||||
await page.locator("#global").getByRole("button", { name: "Submit" }).click();
|
await page.locator("#global").getByRole("button", { name: "Submit" }).click();
|
||||||
await expect(page.getByText("Successfully updated global")).toBeVisible();
|
await expect(page.getByText("Successfully updated global")).toBeVisible();
|
||||||
await page.getByLabel("Favicon:").click();
|
await page.getByLabel("Favicon:").click();
|
||||||
@@ -235,15 +239,15 @@ test.describe.serial("Website tests", () => {
|
|||||||
test("Create/Update legal information", async ({ authenticatedPage: page }) => {
|
test("Create/Update legal information", async ({ authenticatedPage: page }) => {
|
||||||
await page.getByRole("link", { name: "Blog" }).click();
|
await page.getByRole("link", { name: "Blog" }).click();
|
||||||
await page.getByRole("link", { name: "Legal information" }).click();
|
await page.getByRole("link", { name: "Legal information" }).click();
|
||||||
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click();
|
await page.getByLabel("Main content:").click();
|
||||||
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content");
|
await page.getByLabel("Main content:").fill("## Content");
|
||||||
await page.getByRole("button", { name: "Submit" }).click();
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
await expect(page.getByText("Successfully created legal")).toBeVisible();
|
await expect(page.getByText("Successfully created/updated legal")).toBeVisible();
|
||||||
|
|
||||||
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click();
|
await page.getByLabel("Main content:").click();
|
||||||
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content updated");
|
await page.getByLabel("Main content:").fill("## Content updated");
|
||||||
await page.getByRole("button", { name: "Submit" }).click();
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
await expect(page.getByText("Successfully updated legal")).toBeVisible();
|
await expect(page.getByText("Successfully created/updated legal")).toBeVisible();
|
||||||
});
|
});
|
||||||
test("Delete legal information", async ({ authenticatedPage: page }) => {
|
test("Delete legal information", async ({ authenticatedPage: page }) => {
|
||||||
await page.getByRole("link", { name: "Blog" }).click();
|
await page.getByRole("link", { name: "Blog" }).click();
|
||||||
@@ -261,8 +265,8 @@ test.describe.serial("Website tests", () => {
|
|||||||
await page.getByRole("link", { name: "Documentation" }).click();
|
await page.getByRole("link", { name: "Documentation" }).click();
|
||||||
await page.getByRole("link", { name: "Categories" }).click();
|
await page.getByRole("link", { name: "Categories" }).click();
|
||||||
await page.getByRole("button", { name: "Create category" }).click();
|
await page.getByRole("button", { name: "Create category" }).click();
|
||||||
await page.getByLabel("Name:").click();
|
await page.getByLabel("Name:").nth(0).click();
|
||||||
await page.getByLabel("Name:").fill("Category");
|
await page.getByLabel("Name:").nth(0).fill("Category");
|
||||||
await page.getByLabel("Weight:").click();
|
await page.getByLabel("Weight:").click();
|
||||||
await page.getByLabel("Weight:").fill("1000");
|
await page.getByLabel("Weight:").fill("1000");
|
||||||
await page.getByRole("button", { name: "Submit" }).click();
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
@@ -294,8 +298,8 @@ test.describe.serial("Website tests", () => {
|
|||||||
await page.getByRole("link", { name: "Documentation" }).click();
|
await page.getByRole("link", { name: "Documentation" }).click();
|
||||||
await page.getByRole("link", { name: "Categories" }).click();
|
await page.getByRole("link", { name: "Categories" }).click();
|
||||||
await page.getByRole("button", { name: "Create category" }).click();
|
await page.getByRole("button", { name: "Create category" }).click();
|
||||||
await page.getByLabel("Name:").click();
|
await page.getByLabel("Name:").nth(0).click();
|
||||||
await page.getByLabel("Name:").fill("Category");
|
await page.getByLabel("Name:").nth(0).fill("Category");
|
||||||
await page.getByLabel("Weight:").click();
|
await page.getByLabel("Weight:").click();
|
||||||
await page.getByLabel("Weight:").fill("1000");
|
await page.getByLabel("Weight:").fill("1000");
|
||||||
await page.getByRole("button", { name: "Submit" }).click();
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
|
|||||||
Reference in New Issue
Block a user