Basic functions, Home and Community Tab

This commit is contained in:
Malte Schröder
2025-12-17 20:41:51 +01:00
commit 506f12adc2
28 changed files with 1086 additions and 0 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[target.wasm32-unknown-unknown]
rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""]

1
.env Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL="sqlite:community.db"

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

42
Cargo.toml Normal file
View File

@@ -0,0 +1,42 @@
[package]
name = "bytemalte_de"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
leptos = { version = "0.8.0" }
leptos_router = { version = "0.8.0" }
leptos_meta = { version = "0.8.0" }
chrono = { version = "0.4.42", features = ["clock"] }
getrandom = { version = "0.3", features = ["wasm_js"] }
bcrypt = { version = "0.17", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
wasm-bindgen = { version = "=0.2.105", optional = true }
axum = { version = "0.8.0", optional = true }
leptos_axum = { version = "0.8.0", optional = true }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
sqlx = { version = "0.8.2", features = ["runtime-tokio-rustls", "sqlite", "chrono"], optional = true }
serde = "1.0.228"
[features]
hydrate = ["leptos/hydrate", "dep:console_error_panic_hook", "dep:wasm-bindgen"]
ssr = [
"dep:axum", "dep:tokio", "dep:leptos_axum", "dep:sqlx", "dep:bcrypt",
"leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr"
]
[package.metadata.leptos]
output-name = "bytemalte_de"
site-root = "target/site"
site-pkg-dir = "pkg"
style-file = "style/main.scss"
assets-dir = "public"
site-addr = "127.0.0.1:3000"
reload-port = 3001
bin-features = ["ssr"]
bin-default-features = false
lib-features = ["hydrate"]
lib-default-features = false

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

91
README.md Normal file
View File

@@ -0,0 +1,91 @@
<picture>
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
</picture>
# Leptos Axum Starter Template
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool using [Axum](https://github.com/tokio-rs/axum).
## Creating your template repo
If you don't have `cargo-leptos` installed you can install it with
```bash
cargo install cargo-leptos --locked
```
Then run
```bash
cargo leptos new --git https://github.com/leptos-rs/start-axum
```
to generate a new project template.
```bash
cd bytemalte_de
```
to go to your newly created project.
Feel free to explore the project structure, but the best place to start with your application code is in `src/app.rs`.
Additionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`.
## Running your project
```bash
cargo leptos watch
```
## Installing Additional Tools
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
4. `npm install -g sass` - install `dart-sass` (should be optional in future
5. Run `npm install` in end2end subdirectory before test
## Compiling for Release
```bash
cargo leptos build --release
```
Will generate your server binary in target/release and your site package in target/site
## Testing Your Project
```bash
cargo leptos end-to-end
```
```bash
cargo leptos end-to-end --release
```
Cargo-leptos uses Playwright as the end-to-end test tool.
Tests are located in end2end/tests directory.
## Executing a Server on a Remote Machine Without the Toolchain
After running a `cargo leptos build --release` the minimum files needed are:
1. The server binary located in `target/server/release`
2. The `site` directory and all files within located in `target/site`
Copy these files to your remote server. The directory structure should be:
```text
bytemalte_de
site/
```
Set the following environment variables (updating for your project as needed):
```sh
export LEPTOS_OUTPUT_NAME="bytemalte_de"
export LEPTOS_SITE_ROOT="site"
export LEPTOS_SITE_PKG_DIR="pkg"
export LEPTOS_SITE_ADDR="127.0.0.1:3000"
export LEPTOS_RELOAD_PORT="3001"
```
Finally, run the server binary.
## Licensing
This template itself is released under the Unlicense. You should replace the LICENSE for your own application with an appropriate license if you plan to release it publicly.

BIN
community.db Normal file

Binary file not shown.

BIN
community.db-shm Normal file

Binary file not shown.

0
community.db-wal Normal file
View File

3
end2end/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
playwright-report
test-results

167
end2end/package-lock.json generated Normal file
View File

@@ -0,0 +1,167 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.44.1",
"@types/node": "^20.12.12",
"typescript": "^5.4.5"
}
},
"node_modules/@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"license": "MIT"
}
},
"dependencies": {
"@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"requires": {
"playwright": "1.44.1"
}
},
"@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.44.1"
}
},
"playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true
},
"typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true
},
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

15
end2end/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.44.1",
"@types/node": "^20.12.12",
"typescript": "^5.4.5"
}
}

View File

@@ -0,0 +1,105 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices, defineConfig } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
});

View File

@@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
test("homepage has title and heading text", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Welcome to Leptos");
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
});

109
end2end/tsconfig.json Normal file
View File

@@ -0,0 +1,109 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

@@ -0,0 +1,2 @@
CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL);
CREATE TABLE messages (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id));

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

62
src/app.rs Normal file
View File

@@ -0,0 +1,62 @@
use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Title};
use leptos_router::components::{Route, Router, Routes, A};
use leptos_router::path;
use crate::pages::{home::HomePage, community::CommunityPage};
use crate::backend::logic::get_server_time;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
// WICHTIG: Der Schrägstrich / vor pkg sorgt dafür, dass das CSS überall geladen wird
<link rel="stylesheet" id="leptos" href="/pkg/bytemalte_de.css"/>
<AutoReload options=options.clone() />
<HydrationScripts options=options/>
<MetaTags/>
</head>
<body><App/></body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
let time = Resource::new(|| (), |_| async move { get_server_time().await });
view! {
<Title text="Malxtes Community"/>
<Router>
<header class="navbar">
<nav class="nav-content">
<div class="nav-logo">"Malxte."</div>
<div class="nav-links">
<A href="/" exact=true>"Home"</A>
<A href="/community">"Community"</A>
</div>
</nav>
</header>
<main class="page-content">
<Routes fallback=|| "404">
<Route path=path!("") view=HomePage/>
<Route path=path!("/community") view=CommunityPage/>
</Routes>
</main>
<footer class="app-footer">
<Suspense fallback=|| view! { <span class="server-time">"Verbinde..."</span> }>
{move || time.get().map(|t| view! {
<span class="server-time">
"Server Status: Online • " {t.unwrap_or_default()}
</span>
})}
</Suspense>
</footer>
</Router>
}
}

10
src/backend/db.rs Normal file
View File

@@ -0,0 +1,10 @@
#[cfg(feature = "ssr")]
use sqlx::SqlitePool;
#[cfg(feature = "ssr")]
pub async fn build_db_pool() -> SqlitePool {
let database_url = "sqlite:community.db";
let pool = SqlitePool::connect(database_url).await.expect("DB Fehler");
sqlx::migrate!("./migrations").run(&pool).await.expect("Migration Fehler");
pool
}

45
src/backend/logic.rs Normal file
View File

@@ -0,0 +1,45 @@
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ChatMessage {
pub username: String,
pub content: String,
}
#[server]
pub async fn login_or_register(user: String, pass: String) -> Result<i64, ServerFnError> {
use sqlx::SqlitePool;
use bcrypt::{hash, verify, DEFAULT_COST};
let pool = use_context::<SqlitePool>().unwrap();
let existing = sqlx::query!("SELECT id, password_hash FROM users WHERE username = ?", user).fetch_optional(&pool).await?;
if let Some(u) = existing {
if verify(&pass, &u.password_hash).unwrap_or(false) { Ok(u.id.expect("ID fehlt")) }
else { Err(ServerFnError::new("Falsches Passwort")) }
} else {
let hashed = hash(&pass, DEFAULT_COST).unwrap();
let id = sqlx::query!("INSERT INTO users (username, password_hash) VALUES (?, ?)", user, hashed)
.execute(&pool).await?.last_insert_rowid();
Ok(id)
}
}
#[server]
pub async fn send_message(user_id: i64, content: String) -> Result<(), ServerFnError> {
let pool = use_context::<sqlx::SqlitePool>().unwrap();
sqlx::query!("INSERT INTO messages (user_id, content) VALUES (?, ?)", user_id, content).execute(&pool).await?;
Ok(())
}
#[server]
pub async fn get_all_messages() -> Result<Vec<ChatMessage>, ServerFnError> {
let pool = use_context::<sqlx::SqlitePool>().unwrap();
let rows = sqlx::query!("SELECT u.username, m.content FROM messages m JOIN users u ON m.user_id = u.id ORDER BY m.created_at ASC").fetch_all(&pool).await?;
Ok(rows.into_iter().map(|r| ChatMessage { username: r.username, content: r.content }).collect())
}
#[server]
pub async fn get_server_time() -> Result<String, ServerFnError> {
Ok(chrono::Local::now().to_rfc3339())
}

2
src/backend/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod db; // Entferne das #[cfg(feature = "ssr")]
pub mod logic; // Entferne das #[cfg(feature = "ssr")]

12
src/lib.rs Normal file
View File

@@ -0,0 +1,12 @@
pub mod app;
pub mod pages;
pub mod backend;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::app::*;
use leptos::prelude::*;
console_error_panic_hook::set_once();
hydrate_body(App);
}

42
src/main.rs Normal file
View File

@@ -0,0 +1,42 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use bytemalte_de::app::*;
use bytemalte_de::backend::db::build_db_pool;
// Konfiguration laden
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options.clone(); // Klonen für den Router
let addr = leptos_options.site_addr; // Adresse kopieren, bevor wir verschieben
// Datenbank vorbereiten
let db_pool = build_db_pool().await;
let routes = generate_route_list(App);
let app = Router::new()
.leptos_routes_with_context(
&leptos_options,
routes,
{
let pool = db_pool.clone();
move || { provide_context(pool.clone()); }
},
{
let opts = leptos_options.clone();
move || shell(opts.clone())
}
)
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// Server starten mit der vorher gespeicherten Adresse
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
leptos::logging::log!("Server läuft auf http://{}", &addr);
axum::serve(listener, app.into_make_service()).await.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {}

144
src/pages/community.rs Normal file
View File

@@ -0,0 +1,144 @@
use leptos::prelude::*;
use leptos::html::Div;
use crate::backend::logic::*;
#[component]
pub fn CommunityPage() -> impl IntoView {
// --- STATE & SIGNALS ---
// Wer ist eingeloggt? (ID und Benutzername)
let (user_id, set_user_id) = signal(None::<i64>);
let (username, set_username) = signal(String::new());
// Referenz auf das Chat-Fenster für automatisches Scrollen
let scroll_ref = NodeRef::<Div>::new();
// --- DATEN-ABFRAGE (RESOURCE) ---
// Holt die Nachrichten vom Server
let messages = Resource::new(|| (), |_| async move {
get_all_messages().await.unwrap_or_default()
});
// --- ECHTZEIT-POLLING ---
// Fragt alle 2 Sekunden den Server nach neuen Nachrichten (nur im Browser)
#[cfg(not(feature = "ssr"))]
{
use std::time::Duration;
Effect::new(move |_| {
let _ = set_interval_with_handle(move || {
messages.refetch();
}, Duration::from_secs(2));
});
}
// --- AUTO-SCROLL EFFEKT ---
// Immer wenn neue Nachrichten geladen werden, scrollen wir nach ganz unten
Effect::new(move |_| {
messages.get(); // Reagiere auf Änderungen der Nachrichten
if let Some(div) = scroll_ref.get() {
request_animation_frame(move || {
div.set_scroll_top(div.scroll_height());
});
}
});
// --- SERVER ACTIONS ---
let login_action = ServerAction::<LoginOrRegister>::new();
let send_action = ServerAction::<SendMessage>::new();
// --- LOGIK-EFFEKTE ---
// 1. Login-Erfolg verarbeiten
Effect::new(move |_| {
if let Some(Ok(id)) = login_action.value().get() {
set_user_id.set(Some(id));
}
});
// 2. Nach dem Senden sofort Nachrichten aktualisieren
Effect::new(move |_| {
if send_action.value().get().is_some() {
messages.refetch();
}
});
// --- VIEW / UI ---
view! {
<div class="community-container">
{move || match user_id.get() {
// FALL A: Nutzer ist nicht eingeloggt (Anmelde-Karte)
None => view! {
<div class="auth-card">
<h2>"Community Login"</h2>
<p>"Tritt der Diskussion bei und schreibe mit Malxte."</p>
<ActionForm action=login_action>
<input
type="text"
name="user"
placeholder="Dein Name"
on:input:target=move |ev| set_username.set(ev.target().value())
required
/>
<input
type="password"
name="pass"
placeholder="Passwort"
required
/>
<button type="submit">"Beitreten"</button>
</ActionForm>
// Fehlermeldung anzeigen, falls Login fehlschlägt
{move || login_action.value().get().map(|v| match v {
Err(e) => view! { <p style="color: red; margin-top: 10px;">{e.to_string()}</p> }.into_any(),
_ => view! {}.into_any()
})}
</div>
}.into_any(),
// FALL B: Nutzer ist eingeloggt (Chat-Fenster)
Some(id) => view! {
<div class="chat-main">
<header class="chat-top-bar">
<h3>"Globaler Chat"</h3>
<span class="badge">{move || username.get()}</span>
</header>
<div class="message-area" node_ref=scroll_ref>
<Suspense fallback=|| view! { <div class="loading">"Lade Nachrichten..."</div> }>
{move || messages.get().map(|list| {
list.into_iter().map(|msg| {
let is_me = msg.username == username.get();
view! {
<div class=if is_me { "msg-row me" } else { "msg-row other" }>
<div class="msg-bubble">
<strong>{msg.username}</strong>
<p>{msg.content}</p>
</div>
</div>
}
}).collect_view()
})}
</Suspense>
</div>
<div class="chat-input-wrapper">
<ActionForm action=send_action>
// Die ID wird versteckt mitgesendet
<input type="hidden" name="user_id" value=id.to_string() />
<div class="input-group">
<input
type="text"
name="content"
placeholder="Deine Nachricht..."
required
autocomplete="off"
/>
<button type="submit">"Senden"</button>
</div>
</ActionForm>
</div>
</div>
}.into_any()
}}
</div>
}
}

9
src/pages/dashboard.rs Normal file
View File

@@ -0,0 +1,9 @@
use leptos::prelude::*;
#[component]
pub fn DashboardPage() -> impl IntoView {
view! {
<h1>"Dashboard"</h1>
<p>"Hier werden später Community-Statistiken stehen."</p>
}
}

11
src/pages/home.rs Normal file
View File

@@ -0,0 +1,11 @@
use leptos::prelude::*;
#[component]
pub fn HomePage() -> impl IntoView {
view! {
<div class="home-hero">
<h1>"Malxte."</h1>
<p>"Willkommen in der Rust-Community."</p>
</div>
}
}

2
src/pages/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod home;
pub mod community; // Das hat gefehlt!

163
style/main.scss Normal file
View File

@@ -0,0 +1,163 @@
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--bg: #f8fafc;
--white: #ffffff;
--text-main: #0f172a;
--text-light: #64748b;
--border: #e2e8f0;
}
body {
margin: 0;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--bg);
color: var(--text-main);
}
// --- NAVBAR ---
.navbar {
position: sticky;
top: 0;
z-index: 100;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 0.75rem 0;
.nav-content {
max-width: 1000px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1.5rem;
}
.nav-logo { font-size: 1.5rem; font-weight: 800; color: var(--primary); }
.nav-links {
display: flex; gap: 0.5rem;
a {
text-decoration: none; color: var(--text-light); padding: 0.5rem 1rem; border-radius: 0.5rem; transition: all 0.2s;
&[aria-current="page"] { background: rgba(99, 102, 241, 0.1); color: var(--primary); font-weight: 600; }
&:hover { background: rgba(99, 102, 241, 0.05); color: var(--primary); }
}
}
}
// --- HOME PAGE ---
.home-hero {
text-align: center;
padding: 100px 20px;
h1 { font-size: 4rem; color: var(--primary); margin-bottom: 1rem; font-weight: 900; }
p { font-size: 1.25rem; color: var(--text-light); }
}
// --- COMMUNITY / LOGIN ---
.community-container {
display: flex; justify-content: center; padding: 3rem 1rem;
}
.auth-card {
background: var(--white);
padding: 3rem;
border-radius: 1.5rem;
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1);
width: 100%;
max-width: 400px;
text-align: center;
h2 { margin-top: 0; color: var(--text-main); font-size: 1.75rem; }
p { color: var(--text-light); margin-bottom: 2rem; }
// Hier stylen wir die Form-Inhalte direkt
form {
display: flex;
flex-direction: column;
gap: 1.25rem;
input {
width: 100%;
box-sizing: border-box; // Wichtig für korrektes Padding
padding: 0.8rem 1rem;
border: 1px solid var(--border);
border-radius: 0.75rem;
font-size: 1rem;
transition: border-color 0.2s;
&:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); }
}
button {
width: 100%;
padding: 0.8rem;
background: var(--primary);
color: white;
border: none;
border-radius: 0.75rem;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
&:hover { background: var(--primary-dark); }
}
}
}
// --- CHAT WINDOW ---
.chat-main {
background: var(--white); width: 100%; max-width: 800px; height: 650px; border-radius: 1.5rem;
display: flex; flex-direction: column; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.1); overflow: hidden;
}
.chat-top-bar {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
h3 { margin: 0; font-size: 1.1rem; }
.badge { background: #f1f5f9; padding: 0.3rem 0.8rem; border-radius: 1rem; font-size: 0.8rem; font-weight: 600; }
}
.message-area {
flex: 1; overflow-y: auto; padding: 1.5rem; display: flex; flex-direction: column; gap: 1rem; background: #fafafa;
}
.msg-row {
display: flex;
&.me { justify-content: flex-end; .msg-bubble { background: var(--primary); color: white; border-bottom-right-radius: 2px; } }
&.other { justify-content: flex-start; .msg-bubble { background: #f1f5f9; color: var(--text-main); border-bottom-left-radius: 2px; } }
}
.msg-bubble {
padding: 0.75rem 1.25rem; border-radius: 1.25rem; max-width: 75%;
strong { display: block; font-size: 0.75rem; margin-bottom: 4px; opacity: 0.8; }
p { margin: 0; line-height: 1.5; }
}
.chat-input-wrapper {
padding: 1.25rem; border-top: 1px solid var(--border);
form .input-group {
display: flex; gap: 0.75rem;
input { flex: 1; padding: 0.75rem 1.25rem; border-radius: 2rem; border: 1px solid var(--border); background: #f8fafc; }
button { padding: 0 1.5rem; background: var(--primary); color: white; border: none; border-radius: 2rem; font-weight: 600; cursor: pointer; }
}
}
// --- FOOTER & SERVER TIME ---
.app-footer {
text-align: center;
padding: 3rem 0;
.server-time {
display: inline-block;
background: #ffffff;
padding: 0.5rem 1.25rem;
border-radius: 2rem;
border: 1px solid var(--border);
font-size: 0.85rem;
color: var(--text-light);
font-weight: 500;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
}