-
-
-
-
-
- Did you forget to add the page to the router?
-
-
-
+
+
+
+
404
+
Seite nicht gefunden
+
+ Die gesuchte Seite existiert nicht oder wurde verschoben.
+
+
+
Zur Startseite
+
+
);
}
diff --git a/package-lock.json b/package-lock.json
index 03d2afd..29f8d8a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@jridgewell/trace-mapping": "^0.3.25",
+ "@neondatabase/serverless": "^1.0.2",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-aspect-ratio": "^1.1.3",
@@ -39,11 +40,14 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
+ "@types/dompurify": "^3.0.5",
+ "@types/multer": "^2.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"connect-pg-simple": "^10.0.0",
"date-fns": "^3.6.0",
+ "dompurify": "^3.3.1",
"drizzle-orm": "^0.39.3",
"drizzle-zod": "^0.7.0",
"embla-carousel-react": "^8.6.0",
@@ -53,6 +57,7 @@
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"memorystore": "^1.6.7",
+ "multer": "^2.1.0",
"next-themes": "^0.4.6",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
@@ -961,6 +966,28 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@neondatabase/serverless": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.0.2.tgz",
+ "integrity": "sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "^22.15.30",
+ "@types/pg": "^8.8.0"
+ },
+ "engines": {
+ "node": ">=19.0.0"
+ }
+ },
+ "node_modules/@neondatabase/serverless/node_modules/@types/node": {
+ "version": "22.19.13",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
+ "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3014,7 +3041,6 @@
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
@@ -3025,7 +3051,6 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -3106,6 +3131,15 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
+ "node_modules/@types/dompurify": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
+ "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/trusted-types": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -3117,7 +3151,6 @@
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
@@ -3129,7 +3162,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -3152,14 +3184,21 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/@types/multer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
+ "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "20.19.27",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -3202,7 +3241,6 @@
"version": "8.11.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz",
"integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -3221,14 +3259,12 @@
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
@@ -3256,7 +3292,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -3266,13 +3301,18 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT"
+ },
"node_modules/@types/ws": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
@@ -3360,6 +3400,12 @@
"node": ">= 8"
}
},
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "license": "MIT"
+ },
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -3529,6 +3575,12 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
"node_modules/bufferutil": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
@@ -3543,6 +3595,17 @@
"node": ">=6.14.2"
}
},
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -3708,6 +3771,21 @@
"node": ">= 6"
}
},
+ "node_modules/concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "engines": [
+ "node >= 6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
"node_modules/connect-pg-simple": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz",
@@ -3996,6 +4074,15 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/dompurify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
+ "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/drizzle-kit": {
"version": "0.31.8",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.8.tgz",
@@ -5998,6 +6085,68 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/multer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.0.tgz",
+ "integrity": "sha512-TBm6j41rxNohqawsxlsWsNNh/VdV4QFXcBvRcPhXaA05EZ79z0qJ2bQFpync6JBoHTeNY5Q1JpG7AlTjdlfAEA==",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.6.0",
+ "concat-stream": "^2.0.0",
+ "type-is": "^1.6.18"
+ },
+ "engines": {
+ "node": ">= 10.16.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/multer/node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -6117,7 +6266,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/on-finished": {
@@ -6307,7 +6455,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
"integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
- "devOptional": true,
"license": "ISC",
"engines": {
"node": ">=4"
@@ -6332,7 +6479,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz",
"integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
@@ -6594,7 +6740,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
"integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -6604,7 +6749,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"obuf": "~1.1.2"
@@ -6617,7 +6761,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -6627,7 +6770,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -6637,7 +6779,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/prop-types": {
@@ -6933,6 +7074,20 @@
"pify": "^2.3.0"
}
},
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -7370,6 +7525,23 @@
"node": ">= 0.8"
}
},
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -7717,6 +7889,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
@@ -7747,7 +7925,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/unpipe": {
diff --git a/package.json b/package.json
index db705b1..ce43f55 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@jridgewell/trace-mapping": "^0.3.25",
+ "@neondatabase/serverless": "^1.0.2",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-aspect-ratio": "^1.1.3",
@@ -41,11 +42,14 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
+ "@types/dompurify": "^3.0.5",
+ "@types/multer": "^2.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"connect-pg-simple": "^10.0.0",
"date-fns": "^3.6.0",
+ "dompurify": "^3.3.1",
"drizzle-orm": "^0.39.3",
"drizzle-zod": "^0.7.0",
"embla-carousel-react": "^8.6.0",
@@ -55,6 +59,7 @@
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"memorystore": "^1.6.7",
+ "multer": "^2.1.0",
"next-themes": "^0.4.6",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
diff --git a/replit.md b/replit.md
new file mode 100644
index 0000000..ea448d2
--- /dev/null
+++ b/replit.md
@@ -0,0 +1,58 @@
+# news.folx.tv - Blog Platform
+
+## Overview
+A clean, modern blog/news platform for Folx Music Television (news.folx.tv). Dark-themed, content-first design inspired by Medium and The Verge, with support for video embeds from bunny.net, Facebook, Instagram, and TikTok.
+
+## Architecture
+- **Frontend**: React + Vite + TailwindCSS + shadcn/ui (dark mode)
+- **Backend**: Express.js + Node.js
+- **Database**: PostgreSQL with Drizzle ORM
+- **Routing**: wouter (frontend), Express (backend API)
+
+## Key Features
+- Article listing with featured carousel, grid layout, and popular sidebar
+- Individual article pages with full HTML content rendering
+- Category filtering (News, Star-News)
+- HTML content supports embedded iframes (bunny.net, YouTube, Facebook, Instagram, TikTok)
+- DOMPurify sanitization for safe HTML rendering
+- Image upload endpoint (multer) for article images
+- Responsive design with mobile navigation
+- SEO meta tags
+
+## Data Model
+- `articles`: id (serial), title, slug (unique), excerpt, content (HTML), coverImage, category, author, featured, views, publishedAt
+
+## API Endpoints
+- `GET /api/articles` - All articles
+- `GET /api/articles/featured` - Featured articles
+- `GET /api/articles/popular` - Popular articles by views
+- `GET /api/articles/category/:category` - Filter by category
+- `GET /api/articles/:slug` - Single article (increments views)
+- `POST /api/articles` - Create article
+- `PATCH /api/articles/:id` - Update article
+- `DELETE /api/articles/:id` - Delete article
+- `POST /api/upload` - Upload image file
+
+## File Structure
+- `shared/schema.ts` - Drizzle schema + Zod validation
+- `server/db.ts` - Database connection (pg Pool)
+- `server/storage.ts` - Storage interface + DatabaseStorage implementation
+- `server/routes.ts` - API routes + file upload (multer)
+- `server/seed.ts` - Seed data for initial articles
+- `client/src/pages/home.tsx` - Homepage
+- `client/src/pages/article.tsx` - Article detail page
+- `client/src/pages/category.tsx` - Category listing page
+- `client/src/components/header.tsx` - Site header with nav
+- `client/src/components/footer.tsx` - Site footer
+
+## Video Embeds
+Article content (HTML) supports iframe embeds. Allowed domains:
+- iframe.mediadelivery.net / video.bunny.net (Bunny.net)
+- www.facebook.com, www.instagram.com, www.tiktok.com
+- www.youtube.com, player.vimeo.com
+
+## Branding
+- Dark theme by default (class="dark" on html)
+- Primary color: crimson/red (hsl 342 85% 53% light, hsl 9 75% 61% dark)
+- Font: Poppins
+- Logo: Folx TV branding image in header
diff --git a/server/db.ts b/server/db.ts
new file mode 100644
index 0000000..0802cc3
--- /dev/null
+++ b/server/db.ts
@@ -0,0 +1,10 @@
+import { drizzle } from "drizzle-orm/node-postgres";
+import pg from "pg";
+import * as schema from "@shared/schema";
+
+if (!process.env.DATABASE_URL) {
+ throw new Error("DATABASE_URL must be set.");
+}
+
+const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
+export const db = drizzle(pool, { schema });
diff --git a/server/routes.ts b/server/routes.ts
index ae68e01..168e10b 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -1,16 +1,106 @@
import type { Express } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
+import { insertArticleSchema } from "@shared/schema";
+import { seedDatabase } from "./seed";
+import multer from "multer";
+import path from "path";
+import fs from "fs";
+
+const uploadDir = path.join(process.cwd(), "client/public/uploads");
+if (!fs.existsSync(uploadDir)) {
+ fs.mkdirSync(uploadDir, { recursive: true });
+}
+
+const upload = multer({
+ storage: multer.diskStorage({
+ destination: (_req, _file, cb) => cb(null, uploadDir),
+ filename: (_req, file, cb) => {
+ const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
+ const ext = path.extname(file.originalname);
+ cb(null, uniqueSuffix + ext);
+ },
+ }),
+ limits: { fileSize: 10 * 1024 * 1024 },
+ fileFilter: (_req, file, cb) => {
+ const allowed = /jpeg|jpg|png|gif|webp/;
+ const ext = allowed.test(path.extname(file.originalname).toLowerCase());
+ const mime = allowed.test(file.mimetype);
+ cb(null, ext && mime);
+ },
+});
export async function registerRoutes(
httpServer: Server,
app: Express
): Promise
{
- // put application routes here
- // prefix all routes with /api
+ await seedDatabase();
- // use storage to perform CRUD operations on the storage interface
- // e.g. storage.insertUser(user) or storage.getUserByUsername(username)
+ app.get("/api/articles", async (_req, res) => {
+ const articles = await storage.getArticles();
+ res.json(articles);
+ });
+
+ app.get("/api/articles/featured", async (_req, res) => {
+ const articles = await storage.getFeaturedArticles();
+ res.json(articles);
+ });
+
+ app.get("/api/articles/popular", async (req, res) => {
+ const limit = parseInt(req.query.limit as string) || 5;
+ const articles = await storage.getPopularArticles(limit);
+ res.json(articles);
+ });
+
+ app.get("/api/articles/category/:category", async (req, res) => {
+ const articles = await storage.getArticlesByCategory(req.params.category);
+ res.json(articles);
+ });
+
+ app.get("/api/articles/:slug", async (req, res) => {
+ const article = await storage.getArticleBySlug(req.params.slug);
+ if (!article) {
+ return res.status(404).json({ message: "Article not found" });
+ }
+ await storage.incrementViews(article.id);
+ res.json({ ...article, views: article.views + 1 });
+ });
+
+ app.post("/api/articles", async (req, res) => {
+ const parsed = insertArticleSchema.safeParse(req.body);
+ if (!parsed.success) {
+ return res.status(400).json({ message: "Invalid article data", errors: parsed.error.flatten() });
+ }
+ const article = await storage.createArticle(parsed.data);
+ res.status(201).json(article);
+ });
+
+ app.patch("/api/articles/:id", async (req, res) => {
+ const id = parseInt(req.params.id);
+ const partial = insertArticleSchema.partial().safeParse(req.body);
+ if (!partial.success) {
+ return res.status(400).json({ message: "Invalid article data", errors: partial.error.flatten() });
+ }
+ const article = await storage.updateArticle(id, partial.data);
+ if (!article) {
+ return res.status(404).json({ message: "Article not found" });
+ }
+ res.json(article);
+ });
+
+ app.delete("/api/articles/:id", async (req, res) => {
+ const id = parseInt(req.params.id);
+ await storage.deleteArticle(id);
+ res.status(204).send();
+ });
+
+ app.post("/api/upload", upload.single("image"), (req, res) => {
+ if (!req.file) {
+ return res.status(400).json({ message: "No file uploaded" });
+ }
+ const url = `/uploads/${req.file.filename}`;
+ res.json({ url });
+ });
return httpServer;
}
diff --git a/server/seed.ts b/server/seed.ts
new file mode 100644
index 0000000..88b255c
--- /dev/null
+++ b/server/seed.ts
@@ -0,0 +1,113 @@
+import { storage } from "./storage";
+
+export async function seedDatabase() {
+ const existing = await storage.getArticles();
+ if (existing.length > 0) return;
+
+ const seedArticles = [
+ {
+ title: "Oberkrainer goes royal!",
+ slug: "oberkrainer-goes-royal",
+ excerpt: "Wusstest du, dass die Musik von Slavko Avsenik sogar im Buckingham Palace gespielt wurde? Ja, richtig gelesen. Der Sound aus den slowenischen Alpen hat es bis in die Hallen der britischen Monarchie geschafft.",
+ content: `Wusstest du, dass die Musik von Slavko Avsenik sogar im Buckingham Palace gespielt wurde? Ja, richtig gelesen – der Sound aus den slowenischen Alpen hat es bis in die Hallen der britischen Monarchie geschafft.
+
+Die Oberkrainer Musik, die in den 1950er Jahren von Slavko Avsenik und seinen Oberkrainern begründet wurde, hat sich über die Jahrzehnte zu einem wahren Weltphänomen entwickelt. Was als regionale Volksmusik begann, wurde zu einem internationalen Erfolg, der Millionen von Menschen begeistert.
+
+Die Geschichte einer Legende
+
+Slavko Avsenik wurde am 26. November 1929 in Begunje na Gorenjskem geboren. Schon früh zeigte er ein außergewöhnliches musikalisches Talent. Mit seinem Bruder Vilko, der als Texter und Arrangeur fungierte, gründete er die Original Oberkrainer, die zu einer der erfolgreichsten Volksmusikgruppen aller Zeiten werden sollten.
+
+Ihr Markenzeichen: die einzigartige Kombination aus Akkordeon, Klarinette, Trompete, Gitarre und Bariton. Diese Besetzung wurde zum Standard für die gesamte Oberkrainer-Bewegung und inspirierte unzählige Musiker weltweit.
+
+Ein Erbe, das weiterlebt
+
+Heute wird die Tradition der Oberkrainer Musik von einer neuen Generation von Musikern weitergeführt. Bands wie die Saso Avsenik Band und viele andere halten das musikalische Erbe lebendig und bringen es in die moderne Zeit.
`,
+ coverImage: "/images/article-1.png",
+ category: "Star-News",
+ author: "Folx Music Television",
+ featured: true,
+ },
+ {
+ title: "Folx Stadl - Sendung 23: Gaudi und Herzenskl\u00e4nge",
+ slug: "folx-stadl-sendung-23",
+ excerpt: "Folx Stadl, die beliebte TV-Show, die seit November 2017 Fans der Volks- und Schlagermusik begeistert, bringt 2025 spannende Neuerungen.",
+ content: `Folx Stadl, die beliebte TV-Show, die seit November 2017 Fans der Volks- und Schlagermusik begeistert, bringt 2025 spannende Neuerungen. Die Sendung 23 steht ganz im Zeichen von Gaudi und Herzensklängen.
+
+Was erwartet die Zuschauer?
+
+In der neuesten Ausgabe des Folx Stadl präsentieren sich zahlreiche Künstler aus der Volks- und Schlagermusikszene. Die Moderatoren führen gewohnt charmant durch den Abend und sorgen für beste Unterhaltung.
+
+Die Show bietet eine perfekte Mischung aus traditioneller Volksmusik und modernem Schlager. Neben den musikalischen Darbietungen gibt es auch wieder unterhaltsame Gespräche und Überraschungen für das Publikum.
+
+Die Künstler
+
+Unter den auftretenden Künstlern befinden sich sowohl etablierte Größen der Szene als auch aufstrebende Talente. Diese Vielfalt macht den besonderen Reiz des Folx Stadl aus und garantiert einen abwechslungsreichen Abend voller Musik und guter Laune.
`,
+ coverImage: "/images/article-2.png",
+ category: "News",
+ author: "Folx Music Television",
+ featured: true,
+ },
+ {
+ title: "Volksmusik, Pop und jede Menge Gaudi",
+ slug: "volksmusik-pop-gaudi",
+ excerpt: "Eine einzigartige Mischung aus traditioneller Volksmusik und modernem Pop erobert die Herzen der Zuschauer. Das neue Format verbindet Generationen.",
+ content: `Eine einzigartige Mischung aus traditioneller Volksmusik und modernem Pop erobert die Herzen der Zuschauer. Das neue Format verbindet Generationen und zeigt, dass Volksmusik und Pop keine Gegensätze sein müssen.
+
+Tradition trifft Moderne
+
+Die Verbindung von Volksmusik und Pop ist kein neues Phänomen, aber sie erlebt derzeit eine Renaissance. Immer mehr Künstler experimentieren mit der Fusion beider Genres und schaffen dabei etwas völlig Neues.
+
+Was früher undenkbar schien, ist heute Realität: Traditionelle Instrumente wie Akkordeon und Zither treffen auf elektronische Beats und moderne Produktionstechniken. Das Ergebnis ist eine Musik, die sowohl die ältere als auch die jüngere Generation anspricht.
+
+Die Zukunft der Volksmusik
+
+Die Zukunft der Volksmusik liegt in der Offenheit für neue Einflüsse. Während die Wurzeln bewahrt werden, öffnen sich immer mehr Künstler für kreative Experimente. Diese Entwicklung verspricht eine spannende Zukunft für die Volksmusik.
`,
+ coverImage: "/images/article-3.png",
+ category: "News",
+ author: "Folx Music Television",
+ featured: true,
+ },
+ {
+ title: "Frischer Wind bei Folx Stadl - Die 5. Staffel startet in 2025",
+ slug: "folx-stadl-staffel-5",
+ excerpt: "Folx Stadl, die beliebte TV-Show, die seit November 2017 Fans der Volks- und Schlagermusik begeistert, bringt 2025 spannende Neuerungen.",
+ content: `Die fünfte Staffel des Folx Stadl steht in den Startlöchern und verspricht, die bisher aufregendste zu werden. Mit neuen Künstlern, überarbeiteten Bühnensets und noch mehr musikalischer Vielfalt geht die Show in eine neue Runde.
+
+Was ist neu in Staffel 5?
+
+Die Produzenten haben einige Überraschungen vorbereitet. Das Bühnendesign wurde komplett überarbeitet und bietet nun noch mehr Platz für spektakuläre Auftritte. Auch die Lichttechnik wurde modernisiert und sorgt für eine noch beeindruckendere Atmosphäre.
+
+Besonders spannend: Erstmals wird es in jeder Sendung ein spezielles Segment geben, in dem junge Nachwuchskünstler die Möglichkeit erhalten, sich einem breiten Publikum zu präsentieren.
`,
+ coverImage: "/images/article-4.png",
+ category: "News",
+ author: "Folx Music Television",
+ featured: false,
+ },
+ {
+ title: "Folx TV wieder \u00fcber Satellit Astra 19,2\u00b0 Ost zu empfangen",
+ slug: "folx-tv-satellit-astra",
+ excerpt: "Gute Nachrichten f\u00fcr alle Volksmusik-Fans: Folx TV ist ab sofort wieder \u00fcber den Satelliten Astra 19,2\u00b0 Ost zu empfangen. Der Sender ist damit in ganz Europa verf\u00fcgbar.",
+ content: `Gute Nachrichten für alle Volksmusik-Fans: Folx TV ist ab sofort wieder über den Satelliten Astra 19,2° Ost zu empfangen. Der Sender ist damit in ganz Europa verfügbar und kann kostenlos empfangen werden.
+
+Empfangsparameter
+
+Für den Empfang von Folx TV über Astra 19,2° Ost benötigen Sie lediglich eine handelsübliche Satellitenanlage. Der Sender ist unverschlüsselt und kann ohne zusätzliche Kosten empfangen werden.
+
+Die Empfangsparameter sind auf der Website von Folx TV verfügbar. Alternativ können Sie auch einen automatischen Sendersuchlauf durchführen, um Folx TV zu finden.
+
+24 Stunden Volksmusik
+
+Folx TV sendet rund um die Uhr Volksmusik, Schlager und Unterhaltung. Das Programm umfasst Musiksendungen, Konzertmitschnitte, Dokumentationen und natürlich den beliebten Folx Stadl.
`,
+ coverImage: "/images/article-5.png",
+ category: "News",
+ author: "Folx Music Television",
+ featured: false,
+ },
+ ];
+
+ for (const article of seedArticles) {
+ await storage.createArticle(article);
+ }
+
+ console.log("Database seeded with sample articles.");
+}
diff --git a/server/storage.ts b/server/storage.ts
index ee25bd1..8b59771 100644
--- a/server/storage.ts
+++ b/server/storage.ts
@@ -1,38 +1,64 @@
-import { type User, type InsertUser } from "@shared/schema";
-import { randomUUID } from "crypto";
-
-// modify the interface with any CRUD methods
-// you might need
+import { type Article, type InsertArticle, articles } from "@shared/schema";
+import { db } from "./db";
+import { eq, desc, sql } from "drizzle-orm";
export interface IStorage {
- getUser(id: string): Promise;
- getUserByUsername(username: string): Promise;
- createUser(user: InsertUser): Promise;
+ getArticles(): Promise;
+ getArticleBySlug(slug: string): Promise;
+ getArticleById(id: number): Promise;
+ getFeaturedArticles(): Promise;
+ getPopularArticles(limit: number): Promise;
+ getArticlesByCategory(category: string): Promise;
+ createArticle(article: InsertArticle): Promise;
+ updateArticle(id: number, article: Partial): Promise;
+ incrementViews(id: number): Promise;
+ deleteArticle(id: number): Promise;
}
-export class MemStorage implements IStorage {
- private users: Map;
-
- constructor() {
- this.users = new Map();
+export class DatabaseStorage implements IStorage {
+ async getArticles(): Promise {
+ return db.select().from(articles).orderBy(desc(articles.publishedAt));
}
- async getUser(id: string): Promise {
- return this.users.get(id);
+ async getArticleBySlug(slug: string): Promise {
+ const [article] = await db.select().from(articles).where(eq(articles.slug, slug));
+ return article;
}
- async getUserByUsername(username: string): Promise {
- return Array.from(this.users.values()).find(
- (user) => user.username === username,
- );
+ async getArticleById(id: number): Promise {
+ const [article] = await db.select().from(articles).where(eq(articles.id, id));
+ return article;
}
- async createUser(insertUser: InsertUser): Promise {
- const id = randomUUID();
- const user: User = { ...insertUser, id };
- this.users.set(id, user);
- return user;
+ async getFeaturedArticles(): Promise {
+ return db.select().from(articles).where(eq(articles.featured, true)).orderBy(desc(articles.publishedAt)).limit(3);
+ }
+
+ async getPopularArticles(limit: number): Promise {
+ return db.select().from(articles).orderBy(desc(articles.views)).limit(limit);
+ }
+
+ async getArticlesByCategory(category: string): Promise {
+ return db.select().from(articles).where(eq(articles.category, category)).orderBy(desc(articles.publishedAt));
+ }
+
+ async createArticle(article: InsertArticle): Promise {
+ const [created] = await db.insert(articles).values(article).returning();
+ return created;
+ }
+
+ async updateArticle(id: number, article: Partial): Promise {
+ const [updated] = await db.update(articles).set(article).where(eq(articles.id, id)).returning();
+ return updated;
+ }
+
+ async incrementViews(id: number): Promise {
+ await db.update(articles).set({ views: sql`${articles.views} + 1` }).where(eq(articles.id, id));
+ }
+
+ async deleteArticle(id: number): Promise {
+ await db.delete(articles).where(eq(articles.id, id));
}
}
-export const storage = new MemStorage();
+export const storage = new DatabaseStorage();
diff --git a/shared/schema.ts b/shared/schema.ts
index 8e71891..5c78109 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -1,8 +1,31 @@
import { sql } from "drizzle-orm";
-import { pgTable, text, varchar } from "drizzle-orm/pg-core";
+import { pgTable, text, varchar, integer, boolean, timestamp, serial } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
+export const articles = pgTable("articles", {
+ id: serial("id").primaryKey(),
+ title: text("title").notNull(),
+ slug: varchar("slug", { length: 255 }).notNull().unique(),
+ excerpt: text("excerpt").notNull(),
+ content: text("content").notNull(),
+ coverImage: text("cover_image"),
+ category: varchar("category", { length: 100 }).notNull().default("News"),
+ author: varchar("author", { length: 255 }).notNull().default("Folx Music Television"),
+ featured: boolean("featured").notNull().default(false),
+ views: integer("views").notNull().default(0),
+ publishedAt: timestamp("published_at").notNull().defaultNow(),
+});
+
+export const insertArticleSchema = createInsertSchema(articles).omit({
+ id: true,
+ views: true,
+ publishedAt: true,
+});
+
+export type InsertArticle = z.infer;
+export type Article = typeof articles.$inferSelect;
+
export const users = pgTable("users", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
username: text("username").notNull().unique(),