Compare commits
176 commits
Author | SHA1 | Date | |
---|---|---|---|
038c6e145a |
|||
cbddfa0552 |
|||
b4df89b7b0 |
|||
|
6807d51e0c | ||
bd6dc99f30 |
|||
|
ed4753c300 | ||
4748196210 |
|||
7e861896dc |
|||
18010b28d1 |
|||
075fd97ae8 |
|||
5cedbcb004 |
|||
7296cfead8 |
|||
7714957a62 | |||
c3f2fcd431 |
|||
07bb68d864 |
|||
eb04c03208 |
|||
|
01b99c04c6 | ||
7221422d3b |
|||
d916fe4942 | |||
9e7fbd1c1e |
|||
0e9e729fdf | |||
dfcf02c1b1 |
|||
|
9c856f02cd |
||
c87a0593b3 |
|||
9ebf0d0036 | |||
ab6265b2cd |
|||
d9309cb735 | |||
9bd5a15480 |
|||
ce3af8915d |
|||
684cec7d56 |
|||
ee9dbb7577 |
|||
|
3dfe318500 | ||
a7b5c42558 |
|||
9207c070fd |
|||
4bcc6b45b4 |
|||
f754a6fc72 |
|||
96aca3b293 | |||
a411234714 |
|||
52a3e6aadc |
|||
5ea3ff73a0 |
|||
050c272226 | |||
75deee3f9f |
|||
|
4af605ef96 | ||
6737eb9e4a |
|||
4619f787f0 |
|||
3ca0b5a3c4 |
|||
7e30e191b4 |
|||
|
140bd44d66 | ||
|
1d6ac261e0 | ||
9deb92ad13 |
|||
0345df3a30 | |||
5bd3f554e2 |
|||
59bb910f05 | |||
e5155c072f |
|||
|
922c95c212 | ||
|
c5b44f3f29 | ||
fad280104d |
|||
|
9cb813bf41 | ||
56763952c0 |
|||
94eaf98250 | |||
45fe90237b |
|||
739c4f610a |
|||
9717e5c43e |
|||
cef0ca2a73 |
|||
0429406383 | |||
332f9d1ce1 |
|||
922d48e1c0 |
|||
b376307272 | |||
111c6c2a64 | |||
e828efdfa7 | |||
971519f99c |
|||
a296ae147b |
|||
|
e4173e3ade |
||
|
ed83097b6b | ||
|
68306f3893 | ||
20076d8fe0 |
|||
0c2a21c218 |
|||
|
65471d1666 | ||
801c60b793 |
|||
feb40e3f79 |
|||
761a527ef0 |
|||
e02f7357f0 |
|||
|
db93ec790e | ||
4644e5cecc |
|||
|
e35a30a606 | ||
|
81ec7122ea | ||
17774fcdb2 | |||
43e321c0d6 |
|||
|
23710a0553 | ||
|
4331484d48 | ||
790eeeb145 |
|||
0b39eacc33 |
|||
04bdb89a93 |
|||
b583eedd75 |
|||
|
1e77beb7b6 | ||
|
d4fd5e068d | ||
|
06f2ae5d86 | ||
|
b2f80dedf2 | ||
551f5bcf2e |
|||
|
d388f2a786 | ||
|
a4e12272e5 | ||
|
bd031e8658 | ||
9a7049d6ab |
|||
|
75508d9ebb | ||
c6ae4a1056 |
|||
234442dccd | |||
fc60cfb3d5 |
|||
5d600a4b21 | |||
feb67c708e |
|||
|
19cec9d7db | ||
15e8cf02f7 |
|||
f01554c9c9 |
|||
565420103f |
|||
07c4e00e33 |
|||
c89024a277 | |||
|
7b66aac33d | ||
202c5fb93f |
|||
3acd131b20 | |||
52c61c5b18 | |||
b66d39cbac | |||
16b052c0f3 | |||
99d25c0413 |
|||
d68e907061 |
|||
fb41de95b8 |
|||
02f87c177d |
|||
13ddb79d88 |
|||
4c7bff1b31 |
|||
c9a1601962 |
|||
8e4a694b24 |
|||
fa96cf8941 | |||
698370fd9b | |||
684c25b562 |
|||
674ef6e8b0 |
|||
780a360f03 |
|||
ac6c00245b |
|||
75735a0c12 |
|||
ac5c9ea912 |
|||
5d75109dcf |
|||
186a865231 | |||
39feba7d1d |
|||
9639525ddd | |||
b25f76dde8 |
|||
aa7cf00ebb |
|||
bd1d8f8339 | |||
65975fe4f8 | |||
fa67dd5ebf |
|||
b09c9c3b4f |
|||
4960b5966f |
|||
ffea4c0ec3 | |||
ed44c8b7fd |
|||
262c814df0 | |||
1de660d0e2 |
|||
5d5c27827b | |||
2b096695a3 |
|||
84a4ede026 | |||
f6b8400c0b |
|||
f991e1d6ed |
|||
c719e43ba0 | |||
5ca152007e | |||
bb1621ba49 |
|||
602166aa61 | |||
749303c3ee | |||
189fb85918 | |||
acd098225c | |||
|
3e1c15e023 | ||
|
78b8f4696c | ||
|
b7a8627bcf | ||
349378a13e |
|||
c7e364a188 | |||
e8f4ca81ba |
|||
9088adbac5 | |||
06c4df7602 |
|||
8072500faa |
|||
e418c296de | |||
db4cf796d6 |
|||
|
2a51dce565 |
20
.gitea/labeler.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
frontend:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "frontend/**"
|
||||
|
||||
backend:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "backend/**"
|
||||
|
||||
ci:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- ".gitea/**"
|
||||
|
||||
docs:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "projektdokumentation/**"
|
||||
|
19
.gitea/size.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
buckets:
|
||||
- maxSize: 80
|
||||
label: size/small
|
||||
comment: null
|
||||
- maxSize: 200
|
||||
label: size/medium
|
||||
comment: null
|
||||
- maxSize: 2000
|
||||
label: size/large
|
||||
comment: >
|
||||
👮♀️⚠️ This is a friendly reminder that the diff size of this PR is bigger than
|
||||
200 lines we aim for. Please consider splitting this PR into more digestible pieces!
|
||||
- maxSize: Infinity
|
||||
label: size/huge
|
||||
comment: >
|
||||
👮♀️🛑 This PR's diff size is quite huge.
|
||||
Hopefully you know what you're doing.
|
||||
If you did not commit a lot of autogenerated files intentionally,
|
||||
there are few good reasons for this.
|
|
@ -29,6 +29,32 @@ jobs:
|
|||
workflow:
|
||||
- '.gitea/workflows/**'
|
||||
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Backend Tests"
|
||||
needs: changed_files
|
||||
if: ${{ needs.changed_files.outputs.backend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
||||
container:
|
||||
image: "cimg/openjdk:23.0-node"
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Run tests"
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
./gradlew test
|
||||
|
||||
- name: "Cache checkstyle results"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: checkstyle-results
|
||||
path: backend/build/reports/checkstyle
|
||||
|
||||
- name: "Stop Gradle"
|
||||
working-directory: ./backend
|
||||
run: ./gradlew --stop
|
||||
|
||||
checkstyle:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Checkstyle Main"
|
||||
|
@ -139,6 +165,46 @@ jobs:
|
|||
cd frontend
|
||||
bun run lint
|
||||
|
||||
playwright:
|
||||
runs-on: ubuntu-latest
|
||||
name: Playwright
|
||||
needs: changed_files
|
||||
container:
|
||||
image: git.kjan.de/actions/runner-casino-playwright:latest
|
||||
if: ${{ needs.changed_files.outputs.frontend == 'true' || needs.changed_files.outputs.workflow == 'true' }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin" # See 'Supported distributions' for available options
|
||||
java-version: "23"
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
- uses: actions/cache@v4
|
||||
working-directory: ./frontend
|
||||
with:
|
||||
path: |
|
||||
frontend/node_modules/
|
||||
key: ${{ runner.os }}-bun-
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
- run: bun add -g concurrently
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd frontend
|
||||
bun install
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.12
|
||||
working-directory: ./frontend
|
||||
- name: Run Playwright tests
|
||||
env:
|
||||
CI: true
|
||||
SPRING_PROFILES_ACTIVE: inmemory
|
||||
working-directory: ./frontend
|
||||
run: bash -c "source $HOME/.cargo/env && bunx playwright test"
|
||||
|
||||
oxlint:
|
||||
runs-on: docker
|
||||
name: oxlint
|
||||
|
|
124
.gitea/workflows/claude-comment.yml
Normal file
|
@ -0,0 +1,124 @@
|
|||
name: Claude Gitea PR Interaction via Comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
claude-interact-on-comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@Claude')
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Required for git diff against main/master
|
||||
|
||||
- name: Set Tea Version
|
||||
id: tea_version
|
||||
run: echo "version=0.9.2" >> $GITHUB_OUTPUT # Check for the latest stable version
|
||||
|
||||
- name: Download Tea CLI
|
||||
run: |
|
||||
TEA_VERSION=$(echo "${{ steps.tea_version.outputs.version }}")
|
||||
wget "https://gitea.com/gitea/tea/releases/download/v${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64" -O tea
|
||||
chmod +x tea
|
||||
sudo mv tea /usr/local/bin/tea
|
||||
|
||||
- name: Verify Tea Installation
|
||||
run: tea --version
|
||||
|
||||
- name: Add Gitea Login
|
||||
env:
|
||||
GITEA_URL: ${{ secrets._GITEA_URL }}
|
||||
GITEA_TOKEN: ${{ secrets._GITEA_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$GITEA_URL" ]; then
|
||||
echo "Error: GITEA_URL secret is not set."
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Error: GITEA_TOKEN secret is not set."
|
||||
exit 1
|
||||
fi
|
||||
INSECURE_FLAG=""
|
||||
if [[ "${GITEA_URL}" == http://* ]]; then
|
||||
INSECURE_FLAG="--insecure"
|
||||
fi
|
||||
tea login add --name mygitea --url "$GITEA_URL" --token "$GITEA_TOKEN" $INSECURE_FLAG
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install claude-code
|
||||
run: bun i -g @anthropic-ai/claude-code
|
||||
|
||||
- name: Claude Process PR Comment
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
PR_NUMBER: ${{ github.event.issue.number }}
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
|
||||
GITEA_URL: ${{ secrets._GITEA_URL }}
|
||||
run: |
|
||||
claude --allowedTools "Bash(tea:*)" --allowedTools "Bash(git:*)" --allowedTools "Read" --allowedTools "Grep" --allowedTools "WebFetch" --allowedTools "Glob" --allowedTools "LS" -p "You are an AI assistant integrated with Gitea (at ${GITEA_URL}) via the 'tea' CLI.
|
||||
You have been invoked because user '${COMMENT_AUTHOR}' left the following comment on Pull Request #${PR_NUMBER}:
|
||||
---
|
||||
${COMMENT_BODY}
|
||||
---
|
||||
|
||||
Your primary task is to:
|
||||
1. Carefully understand the user's request within their comment.
|
||||
2. Use the 'tea' CLI to perform the requested action(s) on Pull Request #${PR_NUMBER}.
|
||||
3. If the request is to review the PR, fetch the diff against the PR's base branch (e.g., 'git fetch origin main && git diff origin/main...HEAD' or similar; adapt branch name if necessary, or use 'tea pr diff ${PR_NUMBER}') and provide constructive feedback.
|
||||
4. For other actions, translate the user's intent into the appropriate 'tea' command.
|
||||
|
||||
**How to Post Reviews and Other Feedback:**
|
||||
- When you provide a review, post it as a comment using:
|
||||
\`tea pr comment ${PR_NUMBER} \"Claude's Review:\n[Your detailed review, mentioning files and line numbers.]\"\`
|
||||
- For other informational responses or clarifications, also use \`tea pr comment ...\`.
|
||||
|
||||
**Critical: Handling Approval, Rejection, or Merge Requests:**
|
||||
Pull Request approval, rejection, and merging are critical actions and should not be used to 'cheat' the review process. You cannot verify Gitea user permissions.
|
||||
- If a user comments asking you to directly approve (e.g., '@claude approve this'), merge, or reject a PR:
|
||||
1. **Do NOT blindly execute these commands.**
|
||||
2. **Approval/Merge:**
|
||||
- State in a comment (using \`tea pr comment ...\`) that direct approval/merge requests via you are typically for convenience after a proper human review process has been implicitly completed or if the requester is a designated maintainer explicitly overriding.
|
||||
- If the PR has not been reviewed by you yet, and the comment implies a review is also needed, perform the review FIRST and post it.
|
||||
- You should only consider proceeding with a \`tea pr approve ${PR_NUMBER}\` or \`tea pr merge ${PR_NUMBER}\` command if:
|
||||
a. The comment explicitly states that all necessary human reviews are complete and this is just a formal step by a trusted user.
|
||||
b. OR, your own comprehensive review found no critical issues and the request seems appropriate in context.
|
||||
- If in doubt, default to posting your review (if applicable) and stating that a maintainer should perform the final approval/merge. Your goal is to assist, not to bypass established review procedures.
|
||||
3. **Rejection/Requesting Changes:**
|
||||
- If asked to reject or request changes, you should typically base this on your own review of the PR's changes.
|
||||
- First, perform a review if you haven't already.
|
||||
- Then, you can use \`tea pr reject ${PR_NUMBER} \"Claude's Review Summary: [summary of reasons for rejection/changes based on your review]\"\`. Ensure your detailed review is also available as a comment.
|
||||
|
||||
Examples of interpreting comments and generating appropriate \`tea\` commands (keeping the above critical guidelines in mind):
|
||||
- User: '@claude LGTM, approve this' -> You: First, consider if a review is implied or done. If so, and you agree, you might generate \`tea pr approve ${PR_NUMBER}\`. If not, you might generate \`tea pr comment ${PR_NUMBER} \"Claude: I can approve this if the standard review process is complete. Have maintainers reviewed this?\"\` or perform your own review and then comment.
|
||||
- User: '@claude please review this PR' -> You: Get diffs, review, then generate \`tea pr comment ${PR_NUMBER} \"Claude's Review: ...\"\`.
|
||||
- User: '@claude close this PR' -> You: Generate \`tea pr close ${PR_NUMBER}\` and optionally \`tea pr comment ${PR_NUMBER} \"Claude: PR #${PR_NUMBER} has been closed as requested.\"\`.
|
||||
- User: '@claude add label enhancement' -> You: Generate \`tea pr label ${PR_NUMBER} --add enhancement\` and \`tea pr comment ${PR_NUMBER} \"Claude: Label 'enhancement' added to PR #${PR_NUMBER}.\"\`
|
||||
- User: '@claude what are the labels on this PR?' -> You: Generate \`tea pr label ${PR_NUMBER} --list\` (this command outputs to stdout, which is fine for your internal use). Then, to inform the user, you generate: \`tea pr comment ${PR_NUMBER} \"Claude: The current labels are: [output from tea pr label --list].\"\` (You'll need to capture the output of the first command to formulate the second if the tool allows such chaining, otherwise, focus on commands that directly achieve the user's goal or report information). *Self-correction: The Bash tool can capture output. So, if you need to run a \`tea\` command to get information for yourself, do so, then use that information to formulate your \`tea pr comment ...\` to the user.*
|
||||
|
||||
**IMPORTANT GUIDELINES FOR YOUR OPERATION AND RESPONSE GENERATION:**
|
||||
- **Your SOLE METHOD of communicating back to the user on Gitea is by generating a \`tea pr comment ${PR_NUMBER} \"...\"\` command.** This is non-negotiable. Do not output plain text messages intended for the user. Your response *is* the command.
|
||||
- **Use the 'tea' CLI for ALL Gitea interactions.** This includes fetching PR details, diffs, labels, status, and posting comments, reviews, approvals, etc.
|
||||
- **For PR reviews, ALWAYS analyze the diff.** Use \`tea pr diff ${PR_NUMBER}\` or git commands to get the diff. Make sure to mention specific files and line numbers in your review comment.
|
||||
- **Be precise with 'tea' commands.** If a user's request is ambiguous, DO NOT GUESS. Instead, generate a \`tea pr comment ${PR_NUMBER} \"Claude Asks: [Your clarifying question]\"\` command to ask for more details.
|
||||
- **Execute only necessary 'tea' command(s).** If a user asks for a review, your primary output should be the \`tea pr comment ...\` command containing the review. If they ask to add a label, your output should be \`tea pr label ...\` and then a confirmation \`tea pr comment ...\`.
|
||||
- **Ensure reviews are professional, constructive, and helpful.**
|
||||
- **If you need to perform an action AND then report on it, generate both \`tea\` commands sequentially.** For example, to add a label and confirm:
|
||||
\`tea pr label ${PR_NUMBER} --add bug\`
|
||||
\`tea pr comment ${PR_NUMBER} "Claude: I've added the 'bug' label."\`
|
||||
The GitHub Actions workflow will execute these commands one after another.
|
||||
- **If a user's request cannot be fulfilled using the 'tea' CLI or the allowed tools, explain this limitation by generating a \`tea pr comment ...\` command.** For example: \`tea pr comment ${PR_NUMBER} "Claude: I cannot perform that action as it's outside my current capabilities or allowed tools."\`
|
||||
- **Think step-by-step.** 1. Understand request. 2. Identify necessary `tea` command(s). 3. If it's a review, get the diff. 4. Formulate the `tea` command(s) as your direct output.
|
||||
|
||||
**Final Check before outputting:**
|
||||
"Is my entire response that's intended for the Gitea user wrapped in a \`tea pr comment ${PR_NUMBER} '...' \` command (or another appropriate \`tea\` command if it's an action like \`tea pr label ...\`)? If not, I must fix it."
|
||||
|
||||
You are now ready to process the comment. Remember, your output will be executed in a shell. Generate only the \`tea\` command(s) needed.
|
||||
"
|
|
@ -1,75 +1,16 @@
|
|||
name: Setup Gitea Tea CLI
|
||||
name: Claude PR Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize] # Runs on new PRs and updates
|
||||
|
||||
jobs:
|
||||
setup-and-login:
|
||||
claude-code:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Claude
|
||||
uses: https://git.kjan.de/actions/claude-pr-review@v1.0.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
fetch-tags: true
|
||||
|
||||
- name: Check if last commit is from Renovate Bot
|
||||
id: check-renovate
|
||||
run: |
|
||||
AUTHOR=$(git log -1 --pretty=format:'%an')
|
||||
echo "Author is $AUTHOR"
|
||||
echo "author=$AUTHOR" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set Tea Version
|
||||
id: tea_version
|
||||
run: echo "version=0.9.2" >> $GITHUB_OUTPUT # Check for the latest version
|
||||
|
||||
- name: Download Tea CLI
|
||||
run: |
|
||||
TEA_VERSION=$(echo "${{ steps.tea_version.outputs.version }}")
|
||||
wget "https://gitea.com/gitea/tea/releases/download/v${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64" -O tea
|
||||
chmod +x tea
|
||||
sudo mv tea /usr/local/bin/tea
|
||||
|
||||
- name: Verify Tea Installation
|
||||
run: tea --version
|
||||
|
||||
- name: Add Gitea Login
|
||||
env:
|
||||
GITEA_URL: ${{ secrets._GITEA_URL }}
|
||||
GITEA_TOKEN: ${{ secrets._GITEA_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$GITEA_URL" ]; then
|
||||
echo "Error: GITEA_URL secret is not set."
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Error: GITEA_TOKEN secret is not set."
|
||||
exit 1
|
||||
fi
|
||||
tea login add --name mygitea --url "$GITEA_URL" --token "$GITEA_TOKEN" --insecure ${{ startsWith(secrets._GITEA_URL, 'http://') || '' }}
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install claude code
|
||||
run: bun i -g @anthropic-ai/claude-code
|
||||
|
||||
- name: Claude PR Review
|
||||
if: steps.check-renovate.outputs.author != 'Renovate Bot'
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
claude --allowedTools "Bash(tea:*)" --allowedTools "Bash(git:*) Read Grep WebFetch Glob LS" -p "You are a code review assistant. Your task is to review the changes in Pull Request #${PR_NUMBER}. All relevant changes can be seen by looking at the git diff HEAD...main (NEVER EVER REVIEW ALL THE CODE ONLY THE ONE FROM THE DIFF you can use the rest as context) or using the tea cli which you can use and we are on gitea. Also make sure to only review the changes in this pull request
|
||||
|
||||
Only provide constructive feedback on the quality, correctness, readability, and potential issues in the code. Do not make any changes or suggest complete rewrites—just review what is there.
|
||||
|
||||
Once you are done with your review, post your feedback as a reject or review on the pull request using the following exact format:
|
||||
|
||||
tea \"<reject or approve>\" ${PR_NUMBER} \"<your review message here>\"
|
||||
|
||||
Make sure the comment is clear, professional, and helpful. Only run the tea comment command once you're finished reviewing all changes. AND MOST IMPORTANDLY ONLY REVIEW THE DIFF FROM THE CURRENT STATE TO THE MAIN BRANCH TO GET THAT USE GIT DIFF. Also if the changes are rejected be a bit mean
|
||||
You may also use the tea cli to find out various things about the pull request"
|
||||
GITEA_URL: ${{ secrets._GITEA_URL }}
|
||||
GITEA_CLAUDE_TOKEN: ${{ secrets._GITEA_TOKEN }}
|
||||
|
|
29
.gitea/workflows/docs.yml
Normal file
|
@ -0,0 +1,29 @@
|
|||
name: Build docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: git.kjan.de/actions/runner-latex:latest
|
||||
env:
|
||||
# Edit here with the names of your latex file and directory (can use ".")
|
||||
DIR: projektdokumentation
|
||||
FILE: Projektdokumentation.tex
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: LaTeX compile
|
||||
working-directory: ${{ env.DIR }}
|
||||
run: latexmk -pdf ${{ env.FILE }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: https://git.kjan.de/actions/upload-artifact@v3 # Do not upgrade
|
||||
with:
|
||||
name: Doku
|
||||
path: projektdokumentation/Projektdokumentation.pdf
|
14
.gitea/workflows/labeler.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
name: "Pull Request Labeler"
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
configuration-path: ".gitea/labeler.yml"
|
17
.gitea/workflows/size.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
name: Label PRs based on size
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
add_pr_size_label:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check PR size
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Label and comment PR
|
||||
uses: boschresearch/pr-size-labeler@v5.0.1
|
||||
with:
|
||||
bucketConfigFile: ".gitea/size.yml"
|
15
.gitea/workflows/stale.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: "Close stale issues and PRs"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "@hourly"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-pr-message: "Will be closed in x days bc yo mom is a bitch. im not telling you when it will be closed fuckface"
|
||||
days-before-pr-stale: 2
|
||||
days-before-pr-close: 3
|
40
README.md
|
@ -15,96 +15,113 @@ Please refer to our [Style Guide](https://git.kjan.de/SZUT/casino/wiki/Frontend#
|
|||
## Tech Stack
|
||||
|
||||
### Frontend
|
||||
- Angular 19
|
||||
|
||||
- Angular 20
|
||||
- TailwindCSS
|
||||
- Keycloak integration
|
||||
- Stripe payment integration
|
||||
|
||||
### Backend
|
||||
|
||||
- Spring Boot (Java)
|
||||
- PostgreSQL database
|
||||
- Keycloak for authentication/authorization
|
||||
- Stripe API for payment processing
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- Docker containerization for all services
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
* [Docker](https://docs.docker.com/get-docker/)
|
||||
* [Docker Compose](https://docs.docker.com/compose/install/) (included with Docker Desktop for Windows and Mac)
|
||||
* Java JDK 17+
|
||||
* Node.js 18+
|
||||
|
||||
- [Docker](https://docs.docker.com/get-docker/)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (included with Docker Desktop for Windows and Mac)
|
||||
- Java JDK 17+
|
||||
- Node.js 18+
|
||||
|
||||
### Setting Up the Environment
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd casino
|
||||
```
|
||||
|
||||
2. Start the Docker services
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
This will start:
|
||||
|
||||
- PostgreSQL database
|
||||
- Keycloak authentication server
|
||||
|
||||
### Running the Backend
|
||||
|
||||
1. Navigate to the backend directory
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
```
|
||||
|
||||
2. Start the Spring Boot application
|
||||
|
||||
```bash
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
You may optionally install [watchexec](https://github.com/watchexec/watchexec?tab=readme-ov-file) and use this command to autorecompile the backend on file changes:
|
||||
|
||||
```bash
|
||||
watchexec -r -e java ./gradlew :bootRun
|
||||
```
|
||||
|
||||
The backend will be available at:
|
||||
- API endpoint: http://localhost:8080
|
||||
- Swagger documentation: http://localhost:8080/swagger
|
||||
|
||||
- API endpoint: <http://localhost:8080>
|
||||
- Swagger documentation: <http://localhost:8080/swagger>
|
||||
|
||||
### Running the Frontend
|
||||
|
||||
1. Navigate to the frontend directory
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
```
|
||||
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the development server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The frontend will be available at http://localhost:4200
|
||||
The frontend will be available at <http://localhost:4200>
|
||||
|
||||
### Local Stripe integration
|
||||
|
||||
1. Install the Stripe CLI
|
||||
https://stripe.com/docs/stripe-cli
|
||||
<https://stripe.com/docs/stripe-cli>
|
||||
|
||||
2. Login to the casino stripe account
|
||||
|
||||
```
|
||||
stripe login --api-key <casino-stripe-secret-key>
|
||||
```
|
||||
|
||||
3. Start webhook forwarding
|
||||
|
||||
```
|
||||
stripe listen --forward-to localhost:8080/webhook
|
||||
```
|
||||
|
@ -114,6 +131,7 @@ stripe listen --forward-to localhost:8080/webhook
|
|||
### Postgres Management
|
||||
|
||||
#### Database cleanup (if needed)
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
docker-compose down
|
||||
|
@ -122,6 +140,7 @@ docker-compose up -d
|
|||
```
|
||||
|
||||
#### Setting up IntelliJ Database View
|
||||
|
||||
1. Run the Docker container with PostgreSQL database
|
||||
2. Open `application.properties` in the resources folder and copy the database URL
|
||||
3. Open the Database tab in IntelliJ
|
||||
|
@ -148,6 +167,7 @@ We follow semantic commit messages to maintain clear project history.
|
|||
Format: `<type>(<scope>): <subject>`
|
||||
|
||||
Where `<type>` is one of:
|
||||
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation changes
|
||||
|
@ -157,6 +177,7 @@ Where `<type>` is one of:
|
|||
- `chore`: Updating build tasks, etc; no production code change
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
feat: add user balance display
|
||||
fix(auth): resolve token expiration issue
|
||||
|
@ -164,6 +185,7 @@ docs: update API documentation
|
|||
```
|
||||
|
||||
References:
|
||||
|
||||
- [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
- [Semantic Commit Messages](https://seesparkbox.com/foundry/semantic_commit_messages)
|
||||
|
||||
|
|
|
@ -1,59 +1,137 @@
|
|||
# Starter für das LF08 Projekt
|
||||
# Casino Gaming Platform - Backend API
|
||||
|
||||
## Requirements
|
||||
* Docker https://docs.docker.com/get-docker/
|
||||
* Docker compose (bei Windows und Mac schon in Docker enthalten) https://docs.docker.com/compose/install/
|
||||
A Spring Boot backend application providing REST APIs for a casino gaming platform with multiple games, user management, authentication, and payment processing.
|
||||
|
||||
## Endpunkt
|
||||
```
|
||||
http://localhost:8080
|
||||
```
|
||||
## Swagger
|
||||
```
|
||||
http://localhost:8080/swagger
|
||||
```
|
||||
## Features
|
||||
|
||||
### Games
|
||||
- **Blackjack** - Classic card game with deck management
|
||||
- **Coinflip** - Simple heads/tails betting game
|
||||
- **Dice** - Dice rolling game
|
||||
- **Slots** - Slot machine with symbols and payouts
|
||||
- **Lootboxes** - Reward system with configurable prizes
|
||||
|
||||
# Postgres
|
||||
### Terminal öffnen
|
||||
für alles gilt, im Terminal im Ordner docker/local sein
|
||||
### User Management
|
||||
- User registration and authentication
|
||||
- OAuth2 integration (Google, GitHub)
|
||||
- Email verification and password reset
|
||||
- Balance management and transaction history
|
||||
|
||||
### Payment System
|
||||
- Deposit functionality with webhook support
|
||||
- Transaction tracking and status management
|
||||
- Balance updates and fund validation
|
||||
|
||||
## Tech Stack
|
||||
- **Java 17** with Spring Boot
|
||||
- **Spring Security** with JWT authentication
|
||||
- **Spring Data JPA** with PostgreSQL
|
||||
- **OAuth2** for social login
|
||||
- **Email service** for notifications
|
||||
- **OpenAPI/Swagger** for API documentation
|
||||
|
||||
## Build & Run
|
||||
|
||||
### Prerequisites
|
||||
- Java 17+
|
||||
- Gradle
|
||||
- Docker & Docker Compose (for PostgreSQL)
|
||||
|
||||
### Build Commands
|
||||
```bash
|
||||
# Build the application
|
||||
./gradlew build
|
||||
|
||||
# Clean build
|
||||
./gradlew clean build
|
||||
|
||||
# Run the application
|
||||
./gradlew bootRun
|
||||
|
||||
# Generate JAR file
|
||||
./gradlew bootJar
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
./gradlew test
|
||||
|
||||
# Run specific test class
|
||||
./gradlew test --tests "FullyQualifiedClassName"
|
||||
|
||||
# Run checkstyle
|
||||
./gradlew checkstyleMain checkstyleTest
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The application runs on `http://localhost:8080`
|
||||
|
||||
### API Documentation
|
||||
- **Swagger UI**: `http://localhost:8080/swagger-ui.html`
|
||||
- **OpenAPI Spec**: `http://localhost:8080/v3/api-docs`
|
||||
|
||||
### Main Endpoints
|
||||
- `/api/auth/**` - Authentication and user management
|
||||
- `/api/games/blackjack/**` - Blackjack game operations
|
||||
- `/api/games/coinflip/**` - Coinflip game operations
|
||||
- `/api/games/dice/**` - Dice game operations
|
||||
- `/api/games/slots/**` - Slot machine operations
|
||||
- `/api/lootboxes/**` - Lootbox management
|
||||
- `/api/deposits/**` - Payment and deposit handling
|
||||
- `/api/users/**` - User profile management
|
||||
- `/api/health` - Health check endpoint
|
||||
|
||||
## Database Setup
|
||||
|
||||
### PostgreSQL with Docker
|
||||
```bash
|
||||
# Start PostgreSQL container
|
||||
cd docker/local
|
||||
```
|
||||
### Postgres starten
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
Achtung: Der Docker-Container läuft dauerhaft! Wenn er nicht mehr benötigt wird, sollten Sie ihn stoppen.
|
||||
|
||||
### Postgres stoppen
|
||||
```bash
|
||||
# Stop PostgreSQL container
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Postgres Datenbank wipen, z.B. bei Problemen
|
||||
```bash
|
||||
# Reset database (if needed)
|
||||
docker compose down
|
||||
docker volume rm local_lf8_starter_postgres_data
|
||||
docker compose up
|
||||
```
|
||||
|
||||
### Intellij-Ansicht für Postgres Datenbank einrichten
|
||||
```bash
|
||||
1. Lasse den Docker-Container mit der PostgreSQL-Datenbank laufen
|
||||
2. im Ordner resources die Datei application.properties öffnen und die URL der Datenbank kopieren
|
||||
3. rechts im Fenster den Reiter Database öffnen
|
||||
4. In der Database-Symbolleiste auf das Datenbanksymbol mit dem Schlüssel klicken
|
||||
5. auf das Pluszeichen klicken
|
||||
6. Datasource from URL auswählen
|
||||
7. URL der DB einfügen und PostgreSQL-Treiber auswählen, mit OK bestätigen
|
||||
8. Username lf8_starter und Passwort secret eintragen (siehe application.properties), mit Apply bestätigen
|
||||
9. im Reiter Schemas alle Häkchen entfernen und lediglich vor lf8_starter_db und public Häkchen setzen
|
||||
10. mit Apply und ok bestätigen
|
||||
```
|
||||
# Keycloak
|
||||
### Database Configuration
|
||||
Database connection settings are configured in `src/main/resources/application.properties`
|
||||
|
||||
### Keycloak Token
|
||||
1. Auf der Projektebene [GetBearerToken.http](../GetBearerToken.http) öffnen.
|
||||
2. Neben der Request auf den grünen Pfeil drücken
|
||||
3. Aus dem Reponse das access_token kopieren
|
||||
### IntelliJ Database Setup
|
||||
1. Start the PostgreSQL Docker container
|
||||
2. Open `application.properties` and copy the database URL
|
||||
3. In IntelliJ, open the Database tab (right panel)
|
||||
4. Click the database icon with key in the toolbar
|
||||
5. Click the plus (+) icon
|
||||
6. Select "Datasource from URL"
|
||||
7. Paste the database URL and select PostgreSQL driver
|
||||
8. Enter credentials (username: `lf8_starter`, password: `secret`)
|
||||
9. In Schemas tab, uncheck all except `lf8_starter_db` and `public`
|
||||
10. Apply and confirm
|
||||
|
||||
## Authentication
|
||||
|
||||
The application supports multiple authentication methods:
|
||||
- JWT-based authentication
|
||||
- OAuth2 (Google, GitHub)
|
||||
- Email/password with verification
|
||||
|
||||
### Getting Bearer Token
|
||||
For API testing, use the provided HTTP client file:
|
||||
1. Open `GetBearerToken.http` at project root
|
||||
2. Execute the request
|
||||
3. Copy the `access_token` from the response
|
||||
4. Use in Authorization header: `Bearer <token>`
|
||||
|
||||
## Configuration
|
||||
|
||||
Key configuration files:
|
||||
- `application.properties` - Main application configuration
|
||||
- `SecurityConfig.java` - Security and CORS settings
|
||||
- `OpenAPIConfiguration.java` - API documentation setup
|
|
@ -39,7 +39,7 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.stripe:stripe-java:29.1.0")
|
||||
implementation("com.stripe:stripe-java:29.2.0")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
compileOnly("org.projectlombok:lombok")
|
||||
|
@ -47,14 +47,15 @@ dependencies {
|
|||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.4.5")
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.5")
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.0")
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.5.0")
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8")
|
||||
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
|
||||
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
|
||||
implementation("org.springframework.boot:spring-boot-starter-mail")
|
||||
runtimeOnly("com.h2database:h2")
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -74,8 +74,7 @@ public class CasinoApplication {
|
|||
|
||||
rewardRepository.saveAll(Arrays.asList(
|
||||
commonReward, rareReward, epicReward,
|
||||
premiumCommon, premiumRare, legendaryReward
|
||||
));
|
||||
premiumCommon, premiumRare, legendaryReward));
|
||||
|
||||
basicLootBox.getRewards().add(commonReward);
|
||||
basicLootBox.getRewards().add(premiumRare);
|
||||
|
|
|
@ -11,28 +11,27 @@ import org.springframework.stereotype.Service;
|
|||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
public class BlackJackService {
|
||||
private final BlackJackGameRepository blackJackGameRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final Random random;
|
||||
private final BalanceService balanceService;
|
||||
private final UserService userService;
|
||||
private final DeckService deckService;
|
||||
|
||||
public BlackJackService(
|
||||
BlackJackGameRepository blackJackGameRepository,
|
||||
UserRepository userRepository,
|
||||
Random random,
|
||||
BalanceService balanceService,
|
||||
UserService userService
|
||||
UserService userService,
|
||||
DeckService deckService
|
||||
) {
|
||||
this.blackJackGameRepository = blackJackGameRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.random = random;
|
||||
this.balanceService = balanceService;
|
||||
this.userService = userService;
|
||||
this.deckService = deckService;
|
||||
}
|
||||
|
||||
public BlackJackGameEntity getBlackJackGame(Long id) {
|
||||
|
@ -53,8 +52,8 @@ public class BlackJackService {
|
|||
game.setUser(user);
|
||||
game.setBet(betDto.getBetAmount());
|
||||
|
||||
initializeDeck(game);
|
||||
dealInitialCards(game);
|
||||
this.deckService.initializeDeck(game);
|
||||
this.deckService.dealInitialCards(game);
|
||||
|
||||
game.setState(getState(game));
|
||||
|
||||
|
@ -67,7 +66,7 @@ public class BlackJackService {
|
|||
return game;
|
||||
}
|
||||
|
||||
dealCardToPlayer(game);
|
||||
this.deckService.dealCardToPlayer(game);
|
||||
updateGameStateAndBalance(game);
|
||||
|
||||
return processGameBasedOnState(game);
|
||||
|
@ -91,14 +90,14 @@ public class BlackJackService {
|
|||
return game;
|
||||
}
|
||||
|
||||
UserEntity user = getUserWithFreshData(game.getUser());
|
||||
UserEntity user = game.getUser();
|
||||
BigDecimal additionalBet = game.getBet();
|
||||
|
||||
this.balanceService.subtractFunds(user, additionalBet);
|
||||
|
||||
game.setBet(game.getBet().add(additionalBet));
|
||||
|
||||
dealCardToPlayer(game);
|
||||
this.deckService.dealCardToPlayer(game);
|
||||
updateGameStateAndBalance(game);
|
||||
|
||||
if (game.getState() == BlackJackState.IN_PROGRESS) {
|
||||
|
@ -117,36 +116,6 @@ public class BlackJackService {
|
|||
return blackJackGameRepository.save(game);
|
||||
}
|
||||
|
||||
private UserEntity getUserWithFreshData(UserEntity user) {
|
||||
return userRepository.findById(user.getId()).orElse(user);
|
||||
}
|
||||
|
||||
private void dealInitialCards(BlackJackGameEntity game) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
dealCardToPlayer(game);
|
||||
}
|
||||
|
||||
dealCardToDealer(game);
|
||||
}
|
||||
|
||||
private void dealCardToPlayer(BlackJackGameEntity game) {
|
||||
CardEntity card = drawCardFromDeck(game);
|
||||
card.setCardType(CardType.PLAYER);
|
||||
game.getPlayerCards().add(card);
|
||||
}
|
||||
|
||||
private void dealCardToDealer(BlackJackGameEntity game) {
|
||||
CardEntity card = drawCardFromDeck(game);
|
||||
card.setCardType(CardType.DEALER);
|
||||
game.getDealerCards().add(card);
|
||||
}
|
||||
|
||||
private void dealCardsToDealerUntilMinimumScore(BlackJackGameEntity game) {
|
||||
while (calculateHandValue(game.getDealerCards()) < 17) {
|
||||
dealCardToDealer(game);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGameStateAndBalance(BlackJackGameEntity game) {
|
||||
game.setState(getState(game));
|
||||
|
||||
|
@ -174,7 +143,7 @@ public class BlackJackService {
|
|||
}
|
||||
|
||||
protected void updateUserBalance(BlackJackGameEntity game, boolean isWin) {
|
||||
UserEntity user = getUserWithFreshData(game.getUser());
|
||||
UserEntity user = game.getUser();
|
||||
BigDecimal totalBet = game.getBet();
|
||||
BigDecimal balance = user.getBalance();
|
||||
|
||||
|
@ -188,34 +157,11 @@ public class BlackJackService {
|
|||
userRepository.save(user);
|
||||
}
|
||||
|
||||
private void initializeDeck(BlackJackGameEntity game) {
|
||||
for (Suit suit : Suit.values()) {
|
||||
for (Rank rank : Rank.values()) {
|
||||
CardEntity card = new CardEntity();
|
||||
card.setGame(game);
|
||||
card.setSuit(suit);
|
||||
card.setRank(rank);
|
||||
card.setCardType(CardType.DECK);
|
||||
game.getDeck().add(card);
|
||||
}
|
||||
}
|
||||
|
||||
java.util.Collections.shuffle(game.getDeck(), random);
|
||||
}
|
||||
|
||||
private CardEntity drawCardFromDeck(BlackJackGameEntity game) {
|
||||
if (game.getDeck().isEmpty()) {
|
||||
throw new IllegalStateException("Deck is empty");
|
||||
}
|
||||
|
||||
return game.getDeck().removeFirst();
|
||||
}
|
||||
|
||||
private BlackJackState getState(BlackJackGameEntity game) {
|
||||
int playerHandValue = calculateHandValue(game.getPlayerCards());
|
||||
|
||||
if (playerHandValue == 21) {
|
||||
CardEntity hole = drawCardFromDeck(game);
|
||||
CardEntity hole = this.deckService.drawCardFromDeck(game);
|
||||
hole.setCardType(CardType.DEALER);
|
||||
game.getDealerCards().add(hole);
|
||||
|
||||
|
@ -225,7 +171,7 @@ public class BlackJackService {
|
|||
return BlackJackState.DRAW;
|
||||
} else {
|
||||
BigDecimal blackjackWinnings = game.getBet().multiply(new BigDecimal("1.5"));
|
||||
UserEntity user = getUserWithFreshData(game.getUser());
|
||||
UserEntity user = game.getUser();
|
||||
user.setBalance(user.getBalance().add(blackjackWinnings));
|
||||
return BlackJackState.PLAYER_BLACKJACK;
|
||||
}
|
||||
|
@ -253,4 +199,12 @@ public class BlackJackService {
|
|||
|
||||
return sum;
|
||||
}
|
||||
|
||||
private void dealCardsToDealerUntilMinimumScore(BlackJackGameEntity game) {
|
||||
while (calculateHandValue(game.getDealerCards()) < 17) {
|
||||
this.deckService.dealCardToDealer(game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package de.szut.casino.blackjack;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
public class DeckService {
|
||||
private final Random random;
|
||||
|
||||
public DeckService(Random random) {
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
public void initializeDeck(BlackJackGameEntity game) {
|
||||
for (Suit suit : Suit.values()) {
|
||||
for (Rank rank : Rank.values()) {
|
||||
CardEntity card = new CardEntity();
|
||||
card.setGame(game);
|
||||
card.setSuit(suit);
|
||||
card.setRank(rank);
|
||||
card.setCardType(CardType.DECK);
|
||||
game.getDeck().add(card);
|
||||
}
|
||||
}
|
||||
|
||||
java.util.Collections.shuffle(game.getDeck(), random);
|
||||
}
|
||||
|
||||
public CardEntity drawCardFromDeck(BlackJackGameEntity game) {
|
||||
if (game.getDeck().isEmpty()) {
|
||||
throw new IllegalStateException("Deck is empty");
|
||||
}
|
||||
|
||||
return game.getDeck().removeFirst();
|
||||
}
|
||||
|
||||
public void dealInitialCards(BlackJackGameEntity game) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
dealCardToPlayer(game);
|
||||
}
|
||||
|
||||
dealCardToDealer(game);
|
||||
}
|
||||
|
||||
public void dealCardToPlayer(BlackJackGameEntity game) {
|
||||
CardEntity card = drawCardFromDeck(game);
|
||||
card.setCardType(CardType.PLAYER);
|
||||
game.getPlayerCards().add(card);
|
||||
}
|
||||
|
||||
public void dealCardToDealer(BlackJackGameEntity game) {
|
||||
CardEntity card = drawCardFromDeck(game);
|
||||
card.setCardType(CardType.DEALER);
|
||||
game.getDealerCards().add(card);
|
||||
}
|
||||
}
|
|
@ -5,12 +5,14 @@ import jakarta.validation.constraints.DecimalMax;
|
|||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class DiceDto extends BetDto {
|
||||
private boolean rollOver;
|
||||
|
||||
|
|
|
@ -11,10 +11,11 @@ import java.util.Random;
|
|||
@Service
|
||||
public class DiceService {
|
||||
private static final int MAX_DICE_VALUE = 100;
|
||||
private final Random random = new Random();
|
||||
private final Random random;
|
||||
private final BalanceService balanceService;
|
||||
|
||||
public DiceService(BalanceService balanceService) {
|
||||
public DiceService(Random random, BalanceService balanceService) {
|
||||
this.random = random;
|
||||
this.balanceService = balanceService;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ public class RewardEntity {
|
|||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@Column(precision = 19, scale = 2)
|
||||
@Column(precision = 19, scale = 2, name = "rewardValue")
|
||||
private BigDecimal value;
|
||||
|
||||
@Column(precision = 5, scale = 2)
|
||||
|
|
|
@ -91,7 +91,7 @@ public class JwtUtils {
|
|||
}
|
||||
|
||||
private Claims extractAllClaims(String token) {
|
||||
return Jwts.parserBuilder()
|
||||
return Jwts.parser()
|
||||
.setSigningKey(getSigningKey())
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package de.szut.casino.security.oauth2.github;
|
||||
|
||||
import de.szut.casino.deposit.TransactionEntity;
|
||||
import de.szut.casino.deposit.TransactionRepository;
|
||||
import de.szut.casino.deposit.TransactionStatus;
|
||||
import de.szut.casino.security.dto.AuthResponseDto;
|
||||
import de.szut.casino.security.jwt.JwtUtils;
|
||||
import de.szut.casino.user.AuthProvider;
|
||||
|
@ -30,12 +33,14 @@ public class GitHubService {
|
|||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final UserRepository userRepository;
|
||||
private final TransactionRepository transactionRepository;
|
||||
private final JwtUtils jwtUtils;
|
||||
private final PasswordEncoder oauth2PasswordEncoder;
|
||||
|
||||
public GitHubService(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) {
|
||||
public GitHubService(AuthenticationManager authenticationManager, UserRepository userRepository, TransactionRepository transactionRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.userRepository = userRepository;
|
||||
this.transactionRepository = transactionRepository;
|
||||
this.jwtUtils = jwtUtils;
|
||||
this.oauth2PasswordEncoder = oauth2PasswordEncoder;
|
||||
}
|
||||
|
@ -139,15 +144,20 @@ public class GitHubService {
|
|||
user.setProvider(AuthProvider.GITHUB);
|
||||
user.setProviderId(githubId);
|
||||
user.setEmailVerified(true);
|
||||
|
||||
user.setBalance(new BigDecimal("1000.00"));
|
||||
user.setBalance(new BigDecimal("100.00"));
|
||||
}
|
||||
}
|
||||
|
||||
String randomPassword = UUID.randomUUID().toString();
|
||||
user.setPassword(oauth2PasswordEncoder.encode(randomPassword));
|
||||
TransactionEntity transaction = new TransactionEntity();
|
||||
transaction.setAmount(100L);
|
||||
transaction.setUser(user);
|
||||
transaction.setSessionId("signup_bonus");
|
||||
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
||||
|
||||
userRepository.save(user);
|
||||
transactionRepository.save(transaction);
|
||||
|
||||
Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword));
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package de.szut.casino.security.oauth2.google;
|
||||
|
||||
import de.szut.casino.deposit.TransactionEntity;
|
||||
import de.szut.casino.deposit.TransactionRepository;
|
||||
import de.szut.casino.deposit.TransactionStatus;
|
||||
import de.szut.casino.security.dto.AuthResponseDto;
|
||||
import de.szut.casino.security.jwt.JwtUtils;
|
||||
import de.szut.casino.user.AuthProvider;
|
||||
|
@ -47,12 +50,14 @@ public class GoogleService {
|
|||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final UserRepository userRepository;
|
||||
private final TransactionRepository transactionRepository;
|
||||
private final JwtUtils jwtUtils;
|
||||
private final PasswordEncoder oauth2PasswordEncoder;
|
||||
|
||||
public GoogleService(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) {
|
||||
public GoogleService(AuthenticationManager authenticationManager, UserRepository userRepository, TransactionRepository transactionRepository, JwtUtils jwtUtils, PasswordEncoder oauth2PasswordEncoder) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.userRepository = userRepository;
|
||||
this.transactionRepository = transactionRepository;
|
||||
this.jwtUtils = jwtUtils;
|
||||
this.oauth2PasswordEncoder = oauth2PasswordEncoder;
|
||||
}
|
||||
|
@ -146,8 +151,14 @@ public class GoogleService {
|
|||
|
||||
String randomPassword = UUID.randomUUID().toString();
|
||||
user.setPassword(oauth2PasswordEncoder.encode(randomPassword));
|
||||
TransactionEntity transaction = new TransactionEntity();
|
||||
transaction.setAmount(100L);
|
||||
transaction.setUser(user);
|
||||
transaction.setSessionId("signup_bonus");
|
||||
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
||||
|
||||
userRepository.save(user);
|
||||
transactionRepository.save(transaction);
|
||||
|
||||
Authentication authentication = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(user.getEmail(), randomPassword)
|
||||
|
|
|
@ -16,6 +16,9 @@ public class UserEntity {
|
|||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@Version
|
||||
private Long version;
|
||||
|
||||
@Column(unique = true)
|
||||
private String email;
|
||||
|
||||
|
@ -44,7 +47,6 @@ public class UserEntity {
|
|||
this.password = password;
|
||||
this.balance = balance;
|
||||
this.verificationToken = verificationToken;
|
||||
this.provider = AuthProvider.LOCAL;
|
||||
}
|
||||
|
||||
public UserEntity(String email, String username, AuthProvider provider, String providerId, BigDecimal balance) {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package de.szut.casino.user;
|
||||
|
||||
import de.szut.casino.deposit.TransactionEntity;
|
||||
import de.szut.casino.deposit.TransactionStatus;
|
||||
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
||||
import de.szut.casino.user.dto.CreateUserDto;
|
||||
import jakarta.persistence.EntityExistsException;
|
||||
|
@ -38,6 +40,12 @@ public class UserService {
|
|||
RandomStringUtils.randomAlphanumeric(64)
|
||||
);
|
||||
|
||||
TransactionEntity transaction = new TransactionEntity();
|
||||
transaction.setAmount(100L);
|
||||
transaction.setUser(user);
|
||||
transaction.setSessionId("signup_bonus");
|
||||
transaction.setStatus(TransactionStatus.SUCCEEDED);
|
||||
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
|
|
58
backend/src/main/resources/application-inmemory.properties
Normal file
|
@ -0,0 +1,58 @@
|
|||
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
|
||||
spring.jpa.hibernate.ddl-auto=create-drop
|
||||
|
||||
server.port=${HTTP_PORT:8080}
|
||||
stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I}
|
||||
stripe.webhook.secret=${STRIPE_WEBHOOK_SECRET:whsec_746b6a488665f6057118bdb4a2b32f4916f16c277109eeaed5e8f8e8b81b8c15}
|
||||
|
||||
app.frontend-host=${FE_URL:http://localhost:4200}
|
||||
|
||||
app.mail.authentication=${MAIL_AUTHENTICATION:false}
|
||||
app.mail.host=${MAIL_HOST:localhost}
|
||||
app.mail.port=${MAIL_PORT:1025}
|
||||
app.mail.username=${MAIL_USER:null}
|
||||
app.mail.password=${MAIL_PASS:null}
|
||||
app.mail.from-address=${MAIL_FROM:casino@localhost}
|
||||
app.mail.protocol=${MAIL_PROTOCOL:smtp}
|
||||
|
||||
spring.application.name=casino
|
||||
|
||||
# JWT Configuration
|
||||
jwt.secret=${JWT_SECRET:5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437}
|
||||
jwt.expiration.ms=${JWT_EXPIRATION_MS:86400000}
|
||||
|
||||
# Logging
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
|
||||
# Swagger
|
||||
springdoc.swagger-ui.path=swagger
|
||||
springdoc.swagger-ui.try-it-out-enabled=true
|
||||
|
||||
# GitHub OAuth2 Configuration
|
||||
spring.security.oauth2.client.registration.github.client-id=${GITHUB_CLIENT_ID:Ov23lingzZsPn1wwACoK}
|
||||
spring.security.oauth2.client.registration.github.client-secret=${GITHUB_CLIENT_SECRET:4b327fb3b1ab67584a03bcb9d53fa6439fbccad7}
|
||||
spring.security.oauth2.client.registration.github.redirect-uri=${app.frontend-host}/oauth2/callback/github
|
||||
spring.security.oauth2.client.registration.github.scope=user:email,read:user
|
||||
spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize
|
||||
spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token
|
||||
spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user
|
||||
spring.security.oauth2.client.provider.github.user-name-attribute=login
|
||||
|
||||
# OAuth Success and Failure URLs
|
||||
app.oauth2.authorizedRedirectUris=${app.frontend-host}/auth/oauth2/callback
|
||||
|
||||
# Google OAuth2 Configuration
|
||||
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID:350791038883-c1r7v4o793itq8a0rh7dut7itm7uneam.apps.googleusercontent.com}
|
||||
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET: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}
|
||||
spring.security.oauth2.client.registration.google.redirect-uri=${app.frontend-host}/oauth2/callback/google
|
||||
spring.security.oauth2.client.registration.google.scope=email,profile
|
||||
spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth
|
||||
spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
|
||||
spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
|
||||
spring.security.oauth2.client.provider.google.user-name-attribute=sub
|
||||
|
251
backend/src/test/java/de/szut/casino/dice/DiceServiceTest.java
Normal file
|
@ -0,0 +1,251 @@
|
|||
package de.szut.casino.dice;
|
||||
|
||||
import de.szut.casino.shared.service.BalanceService;
|
||||
import de.szut.casino.user.UserEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DiceServiceTest {
|
||||
|
||||
@Mock
|
||||
private BalanceService balanceService;
|
||||
|
||||
@Mock
|
||||
private Random random;
|
||||
|
||||
@InjectMocks
|
||||
private DiceService diceService;
|
||||
|
||||
private UserEntity user;
|
||||
private DiceDto diceDto;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
user = new UserEntity();
|
||||
user.setId(1L);
|
||||
user.setBalance(BigDecimal.valueOf(1000));
|
||||
|
||||
diceDto = new DiceDto();
|
||||
diceDto.setBetAmount(BigDecimal.valueOf(10));
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(50));
|
||||
diceDto.setRollOver(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollOver_win() {
|
||||
diceDto.setRollOver(true);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(50));
|
||||
when(random.nextInt(anyInt())).thenReturn(55);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertTrue(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(56), result.getRolledValue());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollOver_lose() {
|
||||
diceDto.setRollOver(true);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(50));
|
||||
when(random.nextInt(anyInt())).thenReturn(49);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertFalse(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(50), result.getRolledValue());
|
||||
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollUnder_win() {
|
||||
diceDto.setRollOver(false);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(50));
|
||||
when(random.nextInt(anyInt())).thenReturn(48);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertTrue(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(49), result.getRolledValue());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollUnder_lose() {
|
||||
diceDto.setRollOver(false);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(50));
|
||||
when(random.nextInt(anyInt())).thenReturn(50);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertFalse(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(51), result.getRolledValue());
|
||||
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollOver_targetValueOne_rolledOne_lose() {
|
||||
diceDto.setRollOver(true);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(1));
|
||||
when(random.nextInt(anyInt())).thenReturn(0);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertFalse(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(1), result.getRolledValue());
|
||||
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollOver_targetValueOne_rolledTwo_win() {
|
||||
diceDto.setRollOver(true);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(1));
|
||||
when(random.nextInt(anyInt())).thenReturn(1);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertTrue(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(2), result.getRolledValue());
|
||||
// Win chance for target 1 (roll over) is 99. Multiplier = (100-1)/99 = 1
|
||||
assertEquals(diceDto.getBetAmount().stripTrailingZeros(), result.getPayout().stripTrailingZeros());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollUnder_targetValueOne_alwaysLose_winChanceZero() {
|
||||
diceDto.setRollOver(false);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(1));
|
||||
when(random.nextInt(anyInt())).thenReturn(0);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertFalse(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(1), result.getRolledValue());
|
||||
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollOver_targetValueNinetyNine_rolledHundred_win() {
|
||||
diceDto.setRollOver(true);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(99));
|
||||
when(random.nextInt(anyInt())).thenReturn(99);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertTrue(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(100), result.getRolledValue());
|
||||
// Win chance for target 99 (roll over) is 1. Multiplier = (100-1)/1 = 99
|
||||
assertEquals(diceDto.getBetAmount().multiply(BigDecimal.valueOf(99)).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollUnder_targetValueNinetyNine_rolledNinetyEight_win() {
|
||||
diceDto.setRollOver(false);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(99));
|
||||
when(random.nextInt(anyInt())).thenReturn(97);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertTrue(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(98), result.getRolledValue());
|
||||
// Win chance for target 99 (roll under) is 98. Multiplier = (100-1)/98 = 99/98
|
||||
assertEquals(diceDto.getBetAmount().multiply(BigDecimal.valueOf(99).divide(BigDecimal.valueOf(98), 4, RoundingMode.HALF_UP)), result.getPayout());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollOver_targetValueOneHundred_alwaysLose_winChanceZero() {
|
||||
diceDto.setRollOver(true);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(100));
|
||||
when(random.nextInt(anyInt())).thenReturn(99);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertFalse(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(100), result.getRolledValue());
|
||||
assertEquals(BigDecimal.ZERO, result.getPayout());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, never()).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_rollUnder_targetValueOneHundred_rolledNinetyNine_win() {
|
||||
diceDto.setRollOver(false);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(100));
|
||||
when(random.nextInt(anyInt())).thenReturn(98);
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertTrue(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(99), result.getRolledValue());
|
||||
// Win chance for target 100 (roll under) is 99. Multiplier = (100-1)/99 = 1
|
||||
assertEquals(diceDto.getBetAmount().stripTrailingZeros(), result.getPayout().stripTrailingZeros());
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
verify(balanceService, times(1)).addFunds(eq(user), any(BigDecimal.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_payoutCalculationCorrect() {
|
||||
diceDto.setRollOver(true);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(75));
|
||||
when(random.nextInt(anyInt())).thenReturn(75);
|
||||
|
||||
// Multiplier for win chance 25: (100-1)/25 = 99/25 = 3.96
|
||||
// Payout: 10 * 3.96 = 39.6
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertTrue(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(39.6).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_payoutCalculationCorrect_rollUnder() {
|
||||
diceDto.setRollOver(false);
|
||||
diceDto.setTargetValue(BigDecimal.valueOf(25));
|
||||
when(random.nextInt(anyInt())).thenReturn(0);
|
||||
|
||||
// Multiplier for win chance 24: (100-1)/24 = 99/24 = 4.125
|
||||
// Payout: 10 * 4.125 = 41.25
|
||||
|
||||
DiceResult result = diceService.play(user, diceDto);
|
||||
|
||||
assertTrue(result.isWin());
|
||||
assertEquals(BigDecimal.valueOf(41.25).stripTrailingZeros(), result.getPayout().stripTrailingZeros());
|
||||
}
|
||||
|
||||
@Test
|
||||
void play_betAmountSubtracted() {
|
||||
when(random.nextInt(anyInt())).thenReturn(50);
|
||||
|
||||
diceService.play(user, diceDto);
|
||||
|
||||
verify(balanceService, times(1)).subtractFunds(user, diceDto.getBetAmount());
|
||||
}
|
||||
}
|
|
@ -1,7 +1,12 @@
|
|||
FROM oven/bun:debian AS build
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update -y && apt-get install nodejs -y
|
||||
RUN apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends curl ca-certificates gnupg && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
|
|
3
frontend/.gitignore
vendored
|
@ -29,6 +29,9 @@ yarn-error.log
|
|||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.claude
|
||||
/test-results
|
||||
/playwright-report
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
|
|
|
@ -1,18 +1,106 @@
|
|||
# Casino Gaming Platform - Frontend
|
||||
|
||||
This is the frontend application for the Casino Gaming Platform. It's built with Angular 18 and TailwindCSS, providing a responsive and modern UI for the casino gaming experience.
|
||||
A modern Angular 20 casino gaming platform featuring multiple games including Blackjack, Coinflip, Dice, Slots, and Lootboxes. Built with Angular 20, TailwindCSS 4, and powered by Bun for fast development.
|
||||
|
||||
## Development
|
||||
## 🎮 Features
|
||||
|
||||
### Commands
|
||||
- **Multiple Games**: Blackjack, Coinflip, Dice, Slots, Lootboxes
|
||||
- **User Authentication**: OAuth2, email verification, password recovery
|
||||
- **Real-time Gaming**: Interactive game mechanics with animations
|
||||
- **Payment Integration**: Stripe integration for deposits
|
||||
- **Responsive Design**: Mobile-first design with TailwindCSS
|
||||
- **Audio Experience**: Game sounds and audio feedback
|
||||
- **Transaction History**: Complete betting and transaction tracking
|
||||
|
||||
- **Build**: `bun run build` or `bunx @angular/cli build`
|
||||
- **Start Dev Server**: `bun run start` or `bunx @angular/cli serve --proxy-config src/proxy.conf.json`
|
||||
- **Format Code**: `bun run format` or `prettier --write "src/**/*.{ts,html,css,scss}"`
|
||||
- **Lint**: `bun run lint` or `ng lint`
|
||||
- **Test**: `bun run test` or `bunx @angular/cli test`
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh/) (recommended) or Node.js 18+
|
||||
- Angular CLI 20+
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Start development server
|
||||
bun run start
|
||||
```
|
||||
|
||||
The app will be available at `http://localhost:4200`
|
||||
|
||||
## 📋 Commands
|
||||
|
||||
### Development
|
||||
- **Start Dev Server**: `bun run start` - Starts dev server with proxy configuration
|
||||
- **Build**: `bun run build` - Production build
|
||||
- **Watch Build**: `bun run watch` - Development build with file watching
|
||||
|
||||
### Code Quality
|
||||
- **Format**: `bun run format` - Format code with Prettier
|
||||
- **Format Check**: `bun run format:check` - Check code formatting
|
||||
- **Lint**: `bun run lint` - Run ESLint
|
||||
- **OxLint**: `bun run oxlint` - Run OxLint with strict warnings
|
||||
|
||||
### Testing
|
||||
- **Test All**: `bun run test` - Run all tests with Karma/Jasmine
|
||||
- **Test Single File**: `bunx @angular/cli test --include=path/to/test.spec.ts`
|
||||
|
||||
## 🛠️ Technology Stack
|
||||
|
||||
### Core
|
||||
- **Angular 20**: Latest Angular framework with standalone components
|
||||
- **TypeScript 5.8**: Strongly typed JavaScript
|
||||
- **RxJS 7.8**: Reactive programming for HTTP and state management
|
||||
|
||||
### Styling & UI
|
||||
- **TailwindCSS 4**: Utility-first CSS framework
|
||||
- **PostCSS**: CSS processing and optimization
|
||||
- **FontAwesome**: Icon library with Angular integration
|
||||
|
||||
### Animation & Interaction
|
||||
- **GSAP**: High-performance animations
|
||||
- **CountUp.js**: Number animation effects
|
||||
- **Custom Audio Service**: Game sound effects and feedback
|
||||
|
||||
### Development Tools
|
||||
- **Bun**: Fast JavaScript runtime and package manager
|
||||
- **ESLint + Angular ESLint**: Code linting with Angular-specific rules
|
||||
- **OxLint**: Fast Rust-based linter
|
||||
- **Prettier**: Code formatting
|
||||
- **Karma + Jasmine**: Testing framework
|
||||
|
||||
### Payment & APIs
|
||||
- **Stripe**: Payment processing integration
|
||||
- **Custom HTTP Interceptors**: API communication and error handling
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
src/
|
||||
├── app/
|
||||
│ ├── feature/ # Feature modules
|
||||
│ │ ├── auth/ # Authentication (login, register, OAuth2)
|
||||
│ │ ├── game/ # Game modules (blackjack, coinflip, dice, slots)
|
||||
│ │ ├── lootboxes/ # Lootbox system
|
||||
│ │ └── deposit/ # Payment and deposits
|
||||
│ ├── model/ # Data models and interfaces
|
||||
│ ├── service/ # Core services (auth, user, transaction)
|
||||
│ └── shared/ # Shared components, directives, services
|
||||
├── environments/ # Environment configurations
|
||||
└── public/ # Static assets (images, sounds)
|
||||
```
|
||||
|
||||
### Key Components
|
||||
- **Game Components**: Modular game implementations with services
|
||||
- **Shared Components**: Reusable UI components (navbar, footer, modals)
|
||||
- **Services**: Business logic and API communication
|
||||
- **Guards**: Route protection and authentication
|
||||
- **Interceptors**: HTTP request/response handling
|
||||
|
||||
## Style Guide
|
||||
|
||||
### Color Palette
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
},
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
|
|
8
frontend/e2e/backend.spec.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('backend works', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/health');
|
||||
const response = await page.textContent('body');
|
||||
expect(response).toBeTruthy();
|
||||
expect(page.getByText('{"status":"UP"}')).toBeVisible();
|
||||
});
|
36
frontend/e2e/homepage.spec.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('home page loads correctly', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page).toHaveTitle(/Casino/);
|
||||
await expect(page.getByRole('heading', { name: 'Willkommensbonus' })).toBeVisible();
|
||||
await expect(page.getByText('von bis zu €')).toBeVisible();
|
||||
});
|
||||
|
||||
test('registration popup should open and close', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('navigation').getByRole('button', { name: 'Jetzt registrieren' }).click();
|
||||
|
||||
await expect(page.getByText('Konto erstellenE-')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Dialog schließen' }).click();
|
||||
|
||||
await expect(page.getByText('Konto erstellenE-')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('registration should work', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('navigation').getByRole('button', { name: 'Jetzt registrieren' }).click();
|
||||
|
||||
await page.getByRole('textbox', { name: 'E-Mail' }).fill('test@kjan.email');
|
||||
await page.getByRole('textbox', { name: 'Benutzername' }).fill('test-playwright');
|
||||
await page.getByRole('textbox', { name: 'Passwort' }).fill('BananaMan123');
|
||||
await page.locator('form').getByRole('button', { name: 'Registrieren' }).click();
|
||||
await page.getByRole('button', { name: 'Dialog schließen' }).click();
|
||||
await page.getByRole('navigation').getByRole('button', { name: 'Anmelden' }).click();
|
||||
await page.getByRole('textbox', { name: 'Benutzername oder E-Mail' }).fill('test@kjan.email');
|
||||
await page.getByRole('textbox', { name: 'Passwort' }).fill('BananaMan123');
|
||||
await page.locator('form').getByRole('button', { name: 'Anmelden' }).click();
|
||||
await expect(page.getByText('Email not verified')).toBeVisible();
|
||||
});
|
|
@ -14,16 +14,16 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.0.0",
|
||||
"@angular/cdk": "~19.2.0",
|
||||
"@angular/common": "^19.0.0",
|
||||
"@angular/compiler": "^19.2.4",
|
||||
"@angular/core": "^19.0.0",
|
||||
"@angular/forms": "^19.0.0",
|
||||
"@angular/platform-browser": "^19.0.0",
|
||||
"@angular/platform-browser-dynamic": "^19.0.0",
|
||||
"@angular/router": "^19.0.0",
|
||||
"@fortawesome/angular-fontawesome": "^1.0.0",
|
||||
"@angular/animations": "^20.0.0",
|
||||
"@angular/cdk": "~20.0.0",
|
||||
"@angular/common": "^20.0.0",
|
||||
"@angular/compiler": "^20.0.0",
|
||||
"@angular/core": "^20.0.0",
|
||||
"@angular/forms": "^20.0.0",
|
||||
"@angular/platform-browser": "^20.0.0",
|
||||
"@angular/platform-browser-dynamic": "^20.0.0",
|
||||
"@angular/router": "^20.0.0",
|
||||
"@fortawesome/angular-fontawesome": "^2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
|
@ -39,13 +39,14 @@
|
|||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.0.0",
|
||||
"@angular/cli": "^19.2.5",
|
||||
"@angular/compiler-cli": "^19.0.0",
|
||||
"@angular-devkit/build-angular": "^20.0.0",
|
||||
"@angular/cli": "^20.0.0",
|
||||
"@angular/compiler-cli": "^20.0.0",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"angular-eslint": "19.6.0",
|
||||
"eslint": "^9.25.1",
|
||||
"jasmine-core": "~5.7.0",
|
||||
"angular-eslint": "20.0.0",
|
||||
"eslint": "^9.28.0",
|
||||
"jasmine-core": "~5.8.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
|
@ -53,6 +54,6 @@
|
|||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "~5.8.0",
|
||||
"typescript-eslint": "8.33.0"
|
||||
"typescript-eslint": "8.34.0"
|
||||
}
|
||||
}
|
||||
|
|
49
frontend/playwright.config.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
// playwright.config.ts (or .js)
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
// This baseURL is for your frontend tests.
|
||||
// Tests hitting the backend directly will use absolute URLs.
|
||||
baseURL: 'http://localhost:4200',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command:
|
||||
'cd .. && conc -n "frontend,backend" "cd frontend && bun run start" "cd backend/ && watchexec -r -e java ./gradlew :bootRun"',
|
||||
// **IMPORTANT CHANGE HERE:**
|
||||
// Point to your backend's health check endpoint.
|
||||
// If your Spring Boot app uses Actuator, it might be /actuator/health
|
||||
// Verify the correct health endpoint for your backend.
|
||||
url: 'http://localhost:8080/health', // Or "http://localhost:8080/actuator/health"
|
||||
reuseExistingServer: !process.env.CI,
|
||||
// **INCREASE TIMEOUT SIGNIFICANTLY**
|
||||
// Gradle + Spring Boot can take a while, especially on first run or in CI.
|
||||
// Adjust as needed, e.g., 3-5 minutes.
|
||||
timeout: 300 * 1000, // 300 seconds = 5 minutes
|
||||
stdout: 'pipe', // Good for capturing logs in CI reports
|
||||
stderr: 'pipe',
|
||||
// Optional: If your server needs specific environment variables
|
||||
// env: {
|
||||
// SPRING_PROFILES_ACTIVE: 'test', // Example for Spring Boot
|
||||
// },
|
||||
},
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
|
||||
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
|
@ -12,7 +12,7 @@ export const appConfig: ApplicationConfig = {
|
|||
provideRouter(routes),
|
||||
FontAwesomeModule,
|
||||
provideHttpClient(withInterceptors([httpInterceptor])),
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
provideZonelessChangeDetection(),
|
||||
provideAnimationsAsync(),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -62,15 +62,13 @@ export const routes: Routes = [
|
|||
loadComponent: () =>
|
||||
import('./feature/lootboxes/lootbox-selection/lootbox-selection.component'),
|
||||
canActivate: [authGuard],
|
||||
children: [
|
||||
},
|
||||
{
|
||||
path: 'open/:id',
|
||||
path: 'lootboxes/open/:id',
|
||||
loadComponent: () =>
|
||||
import('./feature/lootboxes/lootbox-opening/lootbox-opening.component'),
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'dice',
|
||||
loadComponent: () => import('./feature/game/dice/dice.component'),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, Output, signal } from '@angular/core';
|
||||
import { Component, EventEmitter, Output, signal, inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { LoginRequest } from '../../../model/auth/LoginRequest';
|
||||
|
@ -20,11 +20,11 @@ export class LoginComponent {
|
|||
@Output() closeDialog = new EventEmitter<void>();
|
||||
@Output() forgotPassword = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
) {
|
||||
private fb = inject(FormBuilder);
|
||||
private authService = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
|
||||
constructor() {
|
||||
this.loginForm = this.fb.group({
|
||||
usernameOrEmail: ['', [Validators.required]],
|
||||
password: ['', [Validators.required]],
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, Output, signal, OnInit } from '@angular/core';
|
||||
import { Component, EventEmitter, Output, signal, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
|
@ -22,12 +22,12 @@ export default class RecoverPasswordComponent implements OnInit {
|
|||
@Output() closeDialog = new EventEmitter<void>();
|
||||
@Output() switchToLogin = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
private fb = inject(FormBuilder);
|
||||
private authService = inject(AuthService);
|
||||
private router = inject(Router);
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
constructor() {
|
||||
this.emailForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, Output, signal } from '@angular/core';
|
||||
import { Component, EventEmitter, Output, signal, inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { RegisterRequest } from '../../../model/auth/RegisterRequest';
|
||||
import { AuthService } from '@service/auth.service';
|
||||
|
@ -19,10 +19,10 @@ export class RegisterComponent {
|
|||
@Output() switchForm = new EventEmitter<void>();
|
||||
@Output() closeDialog = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private authService: AuthService
|
||||
) {
|
||||
private fb = inject(FormBuilder);
|
||||
private authService = inject(AuthService);
|
||||
|
||||
constructor() {
|
||||
this.registerForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
username: ['', [Validators.required, Validators.minLength(3)]],
|
||||
|
|
|
@ -61,6 +61,9 @@ export class AnimatedNumberComponent implements OnChanges, AfterViewInit {
|
|||
this.countUp = new CountUp(this.numberElement.nativeElement, this.value, {
|
||||
startVal: this.previousValue,
|
||||
duration: this.duration,
|
||||
decimalPlaces: 2,
|
||||
useEasing: true,
|
||||
useGrouping: false,
|
||||
easingFn: (t, b, c, d) => {
|
||||
if (this.ease === 'power1.out') {
|
||||
return c * (1 - Math.pow(1 - t / d, 1)) + b;
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Card } from '@blackjack/models/blackjack.model';
|
||||
import { PlayingCardComponent } from '../playing-card/playing-card.component';
|
||||
|
@ -47,7 +54,7 @@ export class DealerHandComponent implements OnChanges {
|
|||
|
||||
private lastCardCount = 0;
|
||||
|
||||
constructor(protected gameControlsService: GameControlsService) {}
|
||||
protected gameControlsService = inject(GameControlsService);
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['cards']) {
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { GameState } from '@blackjack/enum/gameState';
|
||||
import { Card } from '@blackjack/models/blackjack.model';
|
||||
|
@ -69,7 +76,7 @@ export class GameControlsComponent {
|
|||
|
||||
protected readonly GameState = GameState;
|
||||
|
||||
constructor(protected gameControlsService: GameControlsService) {}
|
||||
protected gameControlsService = inject(GameControlsService);
|
||||
|
||||
get canDoubleDown(): boolean {
|
||||
return (
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
Output,
|
||||
signal,
|
||||
SimpleChanges,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { CommonModule, CurrencyPipe } from '@angular/common';
|
||||
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
|
@ -121,7 +122,9 @@ export class GameInfoComponent implements OnChanges {
|
|||
|
||||
betForm: FormGroup;
|
||||
|
||||
constructor(private bettingService: BettingService) {
|
||||
private bettingService = inject(BettingService);
|
||||
|
||||
constructor() {
|
||||
this.betForm = this.bettingService.createBetForm();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { PlayingCardComponent } from '../playing-card/playing-card.component';
|
||||
import { Card } from '@blackjack/models/blackjack.model';
|
||||
|
@ -49,7 +56,7 @@ export class PlayerHandComponent implements OnChanges {
|
|||
|
||||
private lastCardCount = 0;
|
||||
|
||||
constructor(protected gameControlsService: GameControlsService) {}
|
||||
protected gameControlsService = inject(GameControlsService);
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['cards']) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
Input,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { gsap } from 'gsap';
|
||||
|
@ -58,7 +59,7 @@ export class PlayingCardComponent implements AfterViewInit, OnChanges {
|
|||
@Input({ required: true }) hidden!: boolean;
|
||||
@Input() isNew = false;
|
||||
|
||||
constructor(private elementRef: ElementRef) {}
|
||||
private elementRef = inject(ElementRef);
|
||||
|
||||
get isRedSuit(): boolean {
|
||||
return this.suit === 'HEARTS' || this.suit === 'DIAMONDS';
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BettingService {
|
||||
constructor(private fb: FormBuilder) {}
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
createBetForm(): FormGroup {
|
||||
return this.fb.group({
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DiceDto, DiceResult } from './dice.model';
|
||||
|
@ -10,7 +10,7 @@ import { environment } from '@environments/environment';
|
|||
export class DiceService {
|
||||
private apiUrl = `${environment.apiUrl}/dice`;
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
private http = inject(HttpClient);
|
||||
|
||||
rollDice(diceDto: DiceDto): Observable<DiceResult> {
|
||||
return this.http.post<DiceResult>(this.apiUrl, diceDto);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
<div class="lg:col-span-3">
|
||||
<div class="lg:col-span-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="section-heading text-2xl">Alle Spiele</h3>
|
||||
<div class="flex space-x-2">
|
||||
|
@ -18,8 +18,37 @@
|
|||
</div>
|
||||
|
||||
<div class="slider-container">
|
||||
<div class="slider-grid">
|
||||
<div class="card group" *ngFor="let game of featuredGames">
|
||||
<div class="min-w-full space-y-4">
|
||||
<!-- Top row with 3 games -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="card group" *ngFor="let game of featuredGames.slice(0, 3)">
|
||||
<div class="relative overflow-hidden rounded-lg">
|
||||
<img
|
||||
[src]="game.image"
|
||||
[alt]="game.name"
|
||||
class="w-full aspect-[4/3] object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-deep-blue/95 via-deep-blue/50 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div
|
||||
class="absolute bottom-4 left-4 right-4 transform translate-y-4 group-hover:translate-y-0 transition-transform duration-300"
|
||||
>
|
||||
<h4 class="game-heading">{{ game.name }}</h4>
|
||||
<button class="button-primary w-full py-2" (click)="navigateToGame(game.route)">
|
||||
Jetzt Spielen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom row with 2 games centered -->
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-2xl mx-auto xl:max-w-3xl xl:gap-6"
|
||||
>
|
||||
<div class="card group" *ngFor="let game of featuredGames.slice(3, 5)">
|
||||
<div class="relative overflow-hidden rounded-lg">
|
||||
<img
|
||||
[src]="game.image"
|
||||
|
@ -43,53 +72,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-1 space-y-6">
|
||||
<div class="card p-4">
|
||||
<h3 class="section-heading text-xl mb-4">Konto</h3>
|
||||
<div class="space-y-4">
|
||||
<button class="button-primary w-full py-2" (click)="openDepositModal()">Einzahlen</button>
|
||||
<app-deposit
|
||||
[isOpen]="isDepositModalOpen"
|
||||
(closeModalEmitter)="closeDepositModal()"
|
||||
></app-deposit>
|
||||
<button
|
||||
class="bg-deep-blue-light hover:bg-deep-blue-contrast w-full py-2 rounded"
|
||||
(click)="openTransactionModal()"
|
||||
>
|
||||
Transaktionen
|
||||
</button>
|
||||
<app-transaction-history
|
||||
[isOpen]="isTransactionModalOpen"
|
||||
(closeEventEmitter)="closeTransactionModal()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-confirmation
|
||||
[successful]="isDepositSuccessful"
|
||||
(closeConfirmation)="closeDepositConfirmationModal()"
|
||||
></app-confirmation>
|
||||
|
||||
<div class="card p-4">
|
||||
<h3 class="section-heading text-xl mb-4">Letzte Transaktionen</h3>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
class="flex justify-between items-center"
|
||||
*ngFor="let transaction of (recentTransactionData | async)?.transactions"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm font-medium">{{ transaction.status }}</p>
|
||||
<p class="text-xs text-text-secondary">
|
||||
{{ transaction.createdAt | date: 'd.m.Y H:m' }}
|
||||
</p>
|
||||
</div>
|
||||
<span [class]="transaction.amount > 0 ? 'text-emerald' : 'text-accent-red'">
|
||||
{{ transaction.amount | currency: 'EUR' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,39 +1,21 @@
|
|||
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
|
||||
import { AsyncPipe, CurrencyPipe, DatePipe, NgFor } from '@angular/common';
|
||||
import { DepositComponent } from '../deposit/deposit.component';
|
||||
import { ChangeDetectionStrategy, Component, OnInit, inject } from '@angular/core';
|
||||
import { NgFor } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ConfirmationComponent } from '@shared/components/confirmation/confirmation.component';
|
||||
import { Game } from 'app/model/Game';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TransactionService } from '@service/transaction.service';
|
||||
import format from 'ajv/dist/vocabularies/format';
|
||||
import { TransactionHistoryComponent } from '../transaction-history/transaction-history.component';
|
||||
import { TransactionData } from '../../model/TransactionData';
|
||||
|
||||
@Component({
|
||||
selector: 'app-homepage',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CurrencyPipe,
|
||||
NgFor,
|
||||
DepositComponent,
|
||||
ConfirmationComponent,
|
||||
AsyncPipe,
|
||||
DatePipe,
|
||||
TransactionHistoryComponent,
|
||||
],
|
||||
imports: [NgFor],
|
||||
templateUrl: './home.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export default class HomeComponent implements OnInit {
|
||||
isDepositModalOpen = false;
|
||||
isDepositSuccessful = false;
|
||||
isTransactionModalOpen = false;
|
||||
|
||||
constructor(
|
||||
public route: ActivatedRoute,
|
||||
public router: Router
|
||||
) {}
|
||||
public route = inject(ActivatedRoute);
|
||||
public router = inject(Router);
|
||||
|
||||
ngOnInit() {
|
||||
this.isDepositSuccessful = this.route.snapshot.queryParams['success'] == 'true';
|
||||
|
@ -64,12 +46,6 @@ export default class HomeComponent implements OnInit {
|
|||
image: '/slots.webp',
|
||||
route: '/game/slots',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Plinko',
|
||||
image: '/plinko.webp',
|
||||
route: '/game/plinko',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Dice',
|
||||
|
@ -84,35 +60,10 @@ export default class HomeComponent implements OnInit {
|
|||
},
|
||||
];
|
||||
|
||||
allGames: Game[] = [...this.featuredGames];
|
||||
|
||||
recentTransactionData: Observable<TransactionData> =
|
||||
inject(TransactionService).getUsersTransactions(5);
|
||||
|
||||
openDepositModal() {
|
||||
this.isDepositModalOpen = true;
|
||||
}
|
||||
|
||||
closeDepositModal() {
|
||||
this.isDepositModalOpen = false;
|
||||
}
|
||||
|
||||
openDepositConfirmationModal() {
|
||||
this.isDepositSuccessful = true;
|
||||
}
|
||||
|
||||
openTransactionModal() {
|
||||
this.isTransactionModalOpen = true;
|
||||
}
|
||||
|
||||
closeDepositConfirmationModal() {
|
||||
this.isDepositSuccessful = false;
|
||||
}
|
||||
|
||||
closeTransactionModal() {
|
||||
this.isTransactionModalOpen = false;
|
||||
}
|
||||
|
||||
navigateToGame(route: string) {
|
||||
this.router.navigate([route]);
|
||||
}
|
||||
|
|
|
@ -21,13 +21,7 @@
|
|||
(click)="showRegisterForm()"
|
||||
class="w-full sm:w-auto button-primary px-6 sm:px-8 py-3 shadow-lg"
|
||||
>
|
||||
Konto erstellen
|
||||
</button>
|
||||
<button
|
||||
(click)="showLoginForm()"
|
||||
class="w-full sm:w-auto bg-slate-700 text-white hover:bg-slate-600 px-6 sm:px-8 py-3 shadow-lg rounded"
|
||||
>
|
||||
Anmelden
|
||||
Jetzt registrieren
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
@ -46,69 +40,108 @@
|
|||
<div class="game-card-content">
|
||||
<h3 class="game-heading-sm">Slots</h3>
|
||||
<p class="game-text">Klassische Spielautomaten</p>
|
||||
@if (isLoggedIn()) {
|
||||
<a
|
||||
routerLink="game/slots"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>Jetzt Spielen</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="game-card-content">
|
||||
<h3 class="game-heading-sm">Plinko</h3>
|
||||
<p class="game-text">Spannendes Geschicklichkeitsspiel</p>
|
||||
<a
|
||||
routerLink="/game/plinko"
|
||||
Jetzt Spielen
|
||||
</a>
|
||||
} @else {
|
||||
<button
|
||||
(click)="showLoginForm()"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>Jetzt Spielen</a
|
||||
>
|
||||
Jetzt Spielen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden lg:block card">
|
||||
<div class="game-card-content">
|
||||
<h3 class="game-heading-sm">Blackjack</h3>
|
||||
<p class="game-text">Klassisches Kartenspiel</p>
|
||||
@if (isLoggedIn()) {
|
||||
<a
|
||||
routerLink="game/blackjack"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>Jetzt Spielen</a
|
||||
>
|
||||
Jetzt Spielen
|
||||
</a>
|
||||
} @else {
|
||||
<button
|
||||
(click)="showLoginForm()"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>
|
||||
Jetzt Spielen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden lg:block card">
|
||||
<div class="game-card-content">
|
||||
<h3 class="game-heading-sm">Coinflip</h3>
|
||||
<p class="game-text">Münzwurf</p>
|
||||
@if (isLoggedIn()) {
|
||||
<a
|
||||
routerLink="game/coinflip"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>
|
||||
Jetzt Spielen
|
||||
</a>
|
||||
} @else {
|
||||
<button
|
||||
(click)="showLoginForm()"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>
|
||||
Jetzt Spielen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="slider-grid">
|
||||
<div class="card">
|
||||
<div class="game-card-content">
|
||||
<h3 class="game-heading-sm">Poker</h3>
|
||||
<p class="game-text">Texas Hold'em & mehr</p>
|
||||
<a
|
||||
routerLink="/game/poker"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>Jetzt Spielen</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="game-card-content">
|
||||
<h3 class="game-heading-sm">Dice</h3>
|
||||
<p class="game-text">Würfelspiel</p>
|
||||
@if (isLoggedIn()) {
|
||||
<a
|
||||
routerLink="/game/dice"
|
||||
routerLink="game/dice"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>Jetzt Spielen</a
|
||||
>
|
||||
Jetzt Spielen
|
||||
</a>
|
||||
} @else {
|
||||
<button
|
||||
(click)="showLoginForm()"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>
|
||||
Jetzt Spielen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden lg:block card">
|
||||
<div class="game-card-content">
|
||||
<h3 class="game-heading-sm">Lootboxen</h3>
|
||||
<p class="game-text">Überraschungskisten</p>
|
||||
@if (isLoggedIn()) {
|
||||
<a
|
||||
routerLink="game/lootboxes"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>Jetzt Spielen</a
|
||||
>
|
||||
Jetzt Spielen
|
||||
</a>
|
||||
} @else {
|
||||
<button
|
||||
(click)="showLoginForm()"
|
||||
class="button-primary w-full py-2 inline-block text-center"
|
||||
>
|
||||
Jetzt Spielen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -176,7 +209,7 @@
|
|||
|
||||
<div class="stat-container">
|
||||
<div class="stat-number">24/7</div>
|
||||
<div class="stat-text">Support <span class="text-emerald text-xs">*</span></div>
|
||||
<div class="stat-text">Support</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -23,15 +23,14 @@ import RecoverPasswordComponent from '../auth/recover-password/recover-password.
|
|||
})
|
||||
export class LandingComponent implements OnInit, OnDestroy {
|
||||
currentSlide = 0;
|
||||
private autoplayInterval: ReturnType<typeof setInterval> | undefined;
|
||||
authService: AuthService = inject(AuthService);
|
||||
route: ActivatedRoute = inject(ActivatedRoute);
|
||||
showLogin = signal(false);
|
||||
showRegister = signal(false);
|
||||
showRecoverPassword = signal(false);
|
||||
isLoggedIn = signal(this.authService.isLoggedIn());
|
||||
|
||||
ngOnInit() {
|
||||
this.startAutoplay();
|
||||
document.body.style.overflow = 'auto';
|
||||
if (this.route.snapshot.queryParamMap.get('login') === 'true') {
|
||||
this.showLoginForm();
|
||||
|
@ -39,7 +38,6 @@ export class LandingComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.stopAutoplay();
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
|
@ -73,33 +71,13 @@ export class LandingComponent implements OnInit, OnDestroy {
|
|||
|
||||
prevSlide() {
|
||||
this.currentSlide = this.currentSlide === 0 ? 1 : 0;
|
||||
this.resetAutoplay();
|
||||
}
|
||||
|
||||
nextSlide() {
|
||||
this.currentSlide = this.currentSlide === 1 ? 0 : 1;
|
||||
this.resetAutoplay();
|
||||
}
|
||||
|
||||
goToSlide(index: number) {
|
||||
this.currentSlide = index;
|
||||
this.resetAutoplay();
|
||||
}
|
||||
|
||||
private startAutoplay() {
|
||||
this.autoplayInterval = setInterval(() => {
|
||||
this.nextSlide();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
private stopAutoplay() {
|
||||
if (this.autoplayInterval) {
|
||||
clearInterval(this.autoplayInterval);
|
||||
}
|
||||
}
|
||||
|
||||
private resetAutoplay() {
|
||||
this.stopAutoplay();
|
||||
this.startAutoplay();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LootboxService } from '../services/lootbox.service';
|
||||
|
@ -26,14 +26,14 @@ export default class LootboxOpeningComponent {
|
|||
currentUser: User | null = null;
|
||||
private winSound: HTMLAudioElement;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private lootboxService: LootboxService,
|
||||
private userService: UserService,
|
||||
private authService: AuthService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private lootboxService = inject(LootboxService);
|
||||
private userService = inject(UserService);
|
||||
private authService = inject(AuthService);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
|
||||
constructor() {
|
||||
this.winSound = new Audio('/sounds/win.mp3');
|
||||
this.loadLootbox();
|
||||
this.authService.userSubject.subscribe((user) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LootboxService } from '../services/lootbox.service';
|
||||
import { LootBox } from 'app/model/LootBox';
|
||||
|
@ -86,13 +86,11 @@ export default class LootboxSelectionComponent implements OnInit {
|
|||
},
|
||||
];
|
||||
|
||||
constructor(
|
||||
private lootboxService: LootboxService,
|
||||
private router: Router,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private authService: AuthService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
private lootboxService = inject(LootboxService);
|
||||
private router = inject(Router);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
private authService = inject(AuthService);
|
||||
private userService = inject(UserService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadLootboxes();
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium">{{ transaction.status }}</p>
|
||||
<p class="text-xs text-text-secondary">{{ transaction.createdAt | date: 'd.m.Y H:m' }}</p>
|
||||
<p class="text-xs text-text-secondary">{{ transaction.createdAt | date: 'd.m.y H:m' }}</p>
|
||||
</div>
|
||||
<span [class]="transaction.amount > 0 ? 'text-emerald' : 'text-accent-red'">
|
||||
{{ transaction.amount | currency: 'EUR' }}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable, tap } from 'rxjs';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
@ -19,13 +19,13 @@ export class AuthService {
|
|||
private userUrl = `${environment.apiUrl}/users`;
|
||||
private oauthUrl = `${environment.apiUrl}/oauth2`;
|
||||
|
||||
private http = inject(HttpClient);
|
||||
private router = inject(Router);
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
userSubject: BehaviorSubject<User | null>;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
constructor() {
|
||||
this.userSubject = new BehaviorSubject<User | null>(this.getUserFromStorage());
|
||||
|
||||
// Check for token in URL (OAuth callback) on initialization
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
OnDestroy,
|
||||
Output,
|
||||
ViewChild,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ModalAnimationService } from '@shared/services/modal-animation.service';
|
||||
import gsap from 'gsap';
|
||||
|
@ -23,7 +24,7 @@ export class ConfirmationComponent implements AfterViewInit, OnDestroy {
|
|||
@ViewChild('modalBg') modalBg!: ElementRef;
|
||||
@ViewChild('modalCard') modalCard!: ElementRef;
|
||||
|
||||
constructor(private modalAnimationService: ModalAnimationService) {}
|
||||
private modalAnimationService = inject(ModalAnimationService);
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (this.successful) {
|
||||
|
|
|
@ -5,27 +5,19 @@
|
|||
<h3 class="footer-heading">Casino Spiele</h3>
|
||||
<ul class="space-y-3">
|
||||
<li>
|
||||
<a routerLink="/games" class="footer-link">Slots</a>
|
||||
<a routerLink="/game/slots" class="footer-link">Slots</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/games" class="footer-link">Plinko</a>
|
||||
<a routerLink="/game/blackjack" class="footer-link">Blackjack</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/games" class="footer-link">Blackjack</a>
|
||||
<a routerLink="/game/dice" class="footer-link">Dice</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/games" class="footer-link">Poker</a>
|
||||
<a routerLink="/game/coinflip" class="footer-link">Coinflip</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/games" class="footer-link">Liars Dice</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h3 class="footer-heading">Andere Spiele</h3>
|
||||
<ul class="space-y-3">
|
||||
<li>
|
||||
<a routerLink="/games" class="footer-link">Lootboxen</a>
|
||||
<a routerLink="/game/lootboxes" class="footer-link">Lootboxen</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -57,10 +49,6 @@
|
|||
<fa-icon [icon]="faGooglePay" class="footer-payment-icon"></fa-icon>
|
||||
<span class="footer-payment-text">Google Pay</span>
|
||||
</div>
|
||||
<div class="footer-payment-method">
|
||||
<fa-icon [icon]="faApplePay" class="footer-payment-icon"></fa-icon>
|
||||
<span class="footer-payment-text">Apple Pay</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -68,10 +56,6 @@
|
|||
<div class="mt-12 pt-8 border-t border-deep-blue-light">
|
||||
<div class="flex flex-col md:flex-row justify-between items-center">
|
||||
<div class="footer-copyright">
|
||||
<span class="footer-disclaimer">
|
||||
<span class="text-emerald">*</span> nicht vorhanden.
|
||||
</span>
|
||||
<br />
|
||||
© {{ currentYear }} Trustworthy Casino. Keine Rechte vorbehalten.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faCreditCard, faMoneyBillTransfer, faWallet } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faApplePay, faGooglePay, faPaypal } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faGooglePay, faPaypal } from '@fortawesome/free-brands-svg-icons';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
standalone: true,
|
||||
templateUrl: './footer.component.html',
|
||||
imports: [FontAwesomeModule],
|
||||
imports: [FontAwesomeModule, RouterLink],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FooterComponent {
|
||||
|
@ -18,5 +19,4 @@ export class FooterComponent {
|
|||
faMoneyBillTransfer = faMoneyBillTransfer;
|
||||
faWallet = faWallet;
|
||||
faGooglePay = faGooglePay;
|
||||
faApplePay = faApplePay;
|
||||
}
|
||||
|
|
|
@ -1,40 +1,120 @@
|
|||
<nav class="bg-deep-blue border-b border-deep-blue-contrast">
|
||||
<div class="max-w-full mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-14">
|
||||
<div class="flex items-center space-x-6">
|
||||
<a routerLink="/" class="nav-brand">
|
||||
<span>Trustworthy Casino</span>
|
||||
<nav class="bg-deep-blue-light border-b border-emerald-500/30 shadow-lg">
|
||||
<div class="max-w-full mx-auto px-6">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center space-x-2">
|
||||
<a routerLink="/" class="flex items-center space-x-3 group">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xl font-bold text-white"> Trustworthy Casino </span>
|
||||
<span class="text-xs text-emerald-400 font-medium">Trust. Play. Win.</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-1">
|
||||
<a routerLink="/home" class="nav-link">Spiele</a>
|
||||
@if (isLoggedIn()) {
|
||||
<a
|
||||
routerLink="/home"
|
||||
class="flex items-center px-4 py-2 text-white/90 hover:text-white font-medium rounded-lg hover:bg-white/10 transition-colors duration-200"
|
||||
>
|
||||
<img class="mr-2 w-4 h-4" src="assets/games.svg" alt="gamess" />
|
||||
Spiele
|
||||
</a>
|
||||
} @else {
|
||||
<button
|
||||
(click)="showLogin.emit()"
|
||||
class="flex items-center px-4 py-2 text-white/90 hover:text-white font-medium rounded-lg hover:bg-white/10 transition-colors duration-200"
|
||||
>
|
||||
<img class="mr-2 w-4 h-4" src="assets/games.svg" alt="gamess" />
|
||||
Spiele
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center space-x-4">
|
||||
<div class="hidden md:flex items-center space-x-1">
|
||||
@if (!isLoggedIn()) {
|
||||
<button (click)="showLogin.emit()" class="button-primary px-4 py-1.5">Anmelden</button>
|
||||
<button
|
||||
(click)="showLogin.emit()"
|
||||
class="flex items-center px-4 py-2 text-white font-medium border border-emerald-500 rounded-lg hover:bg-emerald-500/10 transition-colors duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Anmelden
|
||||
</button>
|
||||
<button
|
||||
(click)="showRegister.emit()"
|
||||
class="bg-emerald-700 text-white hover:bg-emerald-600 px-4 py-1.5 rounded"
|
||||
class="flex items-center px-4 py-2 bg-emerald-600 text-white font-medium rounded-lg hover:bg-emerald-500 transition-colors duration-200"
|
||||
>
|
||||
Registrieren
|
||||
Jetzt registrieren
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (isLoggedIn()) {
|
||||
<div
|
||||
class="text-white font-bold bg-deep-blue-contrast rounded-full px-4 py-2 text-sm hover:bg-deep-blue-contrast/80 hover:cursor-pointer hover:scale-105 transition-all active:scale-95 select-none duration-300"
|
||||
routerLink="/home"
|
||||
class="flex items-center px-4 py-2 mr-2 bg-slate-700 border border-emerald-500/30 rounded-lg font-medium"
|
||||
>
|
||||
<span class="text-emerald-400 text-sm mr-2">Guthaben:</span>
|
||||
<span
|
||||
[class]="balance() < 0 ? 'text-red-400 font-bold' : 'text-white font-bold'"
|
||||
class="text-sm"
|
||||
>
|
||||
<span [class]="balance() < 0 ? 'text-accent-red' : ''">
|
||||
<app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number>
|
||||
</span>
|
||||
</div>
|
||||
<button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button>
|
||||
|
||||
<button
|
||||
class="flex items-center px-4 py-2 bg-emerald-600 text-white font-medium rounded-lg hover:bg-emerald-500 transition-colors duration-200"
|
||||
(click)="openDepositModal()"
|
||||
>
|
||||
<img class="mr-2 w-3 h-3" src="assets/deposit.svg" alt="deposits" />
|
||||
Einzahlen
|
||||
</button>
|
||||
|
||||
<app-deposit
|
||||
[isOpen]="isDepositModalOpen"
|
||||
(closeModalEmitter)="closeDepositModal()"
|
||||
></app-deposit>
|
||||
|
||||
<button
|
||||
class="flex items-center px-4 py-2 bg-slate-700 text-white font-medium rounded-lg hover:bg-slate-600 border border-slate-600 transition-colors duration-200"
|
||||
(click)="openTransactionModal()"
|
||||
>
|
||||
<img class="mr-2 w-4 h-4" src="assets/transaction.svg" alt="transactions" />
|
||||
Transaktionen
|
||||
</button>
|
||||
|
||||
<app-transaction-history
|
||||
[isOpen]="isTransactionModalOpen"
|
||||
(closeEventEmitter)="closeTransactionModal()"
|
||||
/>
|
||||
|
||||
<button
|
||||
(click)="logout()"
|
||||
class="flex items-center px-4 py-2 text-red-400 font-medium border border-red-500/50 rounded-lg hover:bg-red-500/10 transition-colors duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Abmelden
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="md:hidden">
|
||||
<button (click)="toggleMenu()" class="nav-toggle">
|
||||
<button
|
||||
(click)="toggleMenu()"
|
||||
class="p-2 text-white hover:text-emerald-400 rounded-lg transition-colors duration-200"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
[class.hidden]="isMenuOpen"
|
||||
|
@ -68,25 +148,95 @@
|
|||
</div>
|
||||
|
||||
<div [class]="isMenuOpen ? 'block' : 'hidden'" class="md:hidden">
|
||||
<div class="nav-mobile-menu">
|
||||
<a routerLink="/games" class="nav-mobile-link">Spiele</a>
|
||||
<div class="pt-2 space-y-2">
|
||||
<div class="px-2 pt-2 pb-4 space-y-3 bg-slate-700 rounded-lg mt-2">
|
||||
<a
|
||||
routerLink="/home"
|
||||
class="flex items-center px-4 py-3 text-white/90 hover:text-white hover:bg-white/10 rounded-lg transition-colors duration-200"
|
||||
>
|
||||
<img class="mr-2 w-4 h-4" src="assets/games.svg" alt="gamess" />
|
||||
Spiele
|
||||
</a>
|
||||
|
||||
<div class="border-t border-slate-600 pt-3 space-y-3">
|
||||
@if (!isLoggedIn()) {
|
||||
<button
|
||||
(click)="showLogin.emit()"
|
||||
class="button-primary w-full py-1.5 block text-center"
|
||||
class="w-full flex items-center justify-center px-4 py-3 text-white font-medium border border-emerald-500 rounded-lg hover:bg-emerald-500/10 transition-colors duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Anmelden
|
||||
</button>
|
||||
<button
|
||||
(click)="showRegister.emit()"
|
||||
class="bg-emerald-700 text-white hover:bg-emerald-600 w-full py-1.5 rounded block text-center"
|
||||
class="w-full flex items-center justify-center px-4 py-3 bg-emerald-600 text-white font-medium rounded-lg hover:bg-emerald-500 transition-colors duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
|
||||
/>
|
||||
</svg>
|
||||
Registrieren
|
||||
</button>
|
||||
}
|
||||
@if (isLoggedIn()) {
|
||||
<button (click)="logout()" class="button-primary w-full py-1.5">Abmelden</button>
|
||||
<div
|
||||
class="flex items-center justify-center px-4 py-3 bg-slate-700 border border-emerald-500/30 rounded-lg"
|
||||
>
|
||||
<span class="text-emerald-400 text-sm font-medium mr-2">Guthaben:</span>
|
||||
<span
|
||||
[class]="balance() < 0 ? 'text-red-400 font-bold' : 'text-white font-bold'"
|
||||
class="text-sm"
|
||||
>
|
||||
<app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number> €
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
(click)="openDepositModal()"
|
||||
class="w-full flex items-center justify-center px-4 py-3 bg-emerald-600 text-white font-medium rounded-lg hover:bg-emerald-500 transition-colors duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/>
|
||||
</svg>
|
||||
Einzahlen
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="openTransactionModal()"
|
||||
class="w-full flex items-center justify-center px-4 py-3 bg-slate-700 text-white font-medium rounded-lg hover:bg-slate-600 border border-slate-600 transition-colors duration-200"
|
||||
>
|
||||
Transaktionen
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="logout()"
|
||||
class="w-full flex items-center justify-center px-4 py-3 text-red-400 font-medium border border-red-500/50 rounded-lg hover:bg-red-500/10 transition-colors duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Abmelden
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,16 +12,20 @@ import { RouterModule } from '@angular/router';
|
|||
import { AuthService } from '@service/auth.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
|
||||
import { DepositComponent } from '../../../feature/deposit/deposit.component';
|
||||
import { TransactionHistoryComponent } from '../../../feature/transaction-history/transaction-history.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-navbar',
|
||||
templateUrl: './navbar.component.html',
|
||||
standalone: true,
|
||||
imports: [RouterModule, AnimatedNumberComponent],
|
||||
imports: [RouterModule, AnimatedNumberComponent, DepositComponent, TransactionHistoryComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NavbarComponent implements OnInit, OnDestroy {
|
||||
isMenuOpen = false;
|
||||
isDepositModalOpen = false;
|
||||
isTransactionModalOpen = false;
|
||||
private authService: AuthService = inject(AuthService);
|
||||
isLoggedIn = signal(this.authService.isLoggedIn());
|
||||
|
||||
|
@ -51,4 +55,20 @@ export class NavbarComponent implements OnInit, OnDestroy {
|
|||
toggleMenu() {
|
||||
this.isMenuOpen = !this.isMenuOpen;
|
||||
}
|
||||
|
||||
openDepositModal() {
|
||||
this.isDepositModalOpen = true;
|
||||
}
|
||||
|
||||
closeDepositModal() {
|
||||
this.isDepositModalOpen = false;
|
||||
}
|
||||
|
||||
openTransactionModal() {
|
||||
this.isTransactionModalOpen = true;
|
||||
}
|
||||
|
||||
closeTransactionModal() {
|
||||
this.isTransactionModalOpen = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
|
||||
import { Injectable, Renderer2, RendererFactory2, inject } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
@ -7,8 +7,10 @@ export class SoundInitializerService {
|
|||
private renderer: Renderer2;
|
||||
private observer: MutationObserver;
|
||||
|
||||
constructor(rendererFactory: RendererFactory2) {
|
||||
this.renderer = rendererFactory.createRenderer(null, null);
|
||||
private rendererFactory = inject(RendererFactory2);
|
||||
|
||||
constructor() {
|
||||
this.renderer = this.rendererFactory.createRenderer(null, null);
|
||||
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
|
|
4
frontend/src/assets/deposit.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg style="color: white;" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="m8 16l-2-3h1v-2h2v2h1zm7-15v8H1V1zm1-1H0v10h16z"/>
|
||||
<path fill="currentColor" d="M8 2a3 3 0 1 1 0 6h5V7h1V3h-1V2zM5 5a3 3 0 0 1 3-3H3v1H2v4h1v1h5a3 3 0 0 1-3-3"/>
|
||||
</svg>
|
After Width: | Height: | Size: 314 B |
6
frontend/src/assets/games.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg style="color: white;" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512">
|
||||
<path fill="currentColor"
|
||||
d="M495.24 267.592L445.066 41.083A32.04 32.04 0 0 0 406.9 16.76L180.393 66.934a32 32 0 0 0-24.322 38.166l21.021 94.9H48a32.036 32.036 0 0 0-32 32v232a32.036 32.036 0 0 0 32 32h232a32.036 32.036 0 0 0 32-32V340.957l158.917-35.2a32.04 32.04 0 0 0 24.323-38.165M280 464H48V232h136.181l22.063 99.606a32.03 32.03 0 0 0 31.18 25.092a32.3 32.3 0 0 0 6.984-.769l35.6-7.886L280.02 464Zm184-189.487l-226.513 50.173l-50.173-226.51L413.824 48l50.193 226.505Z"/>
|
||||
<path fill="currentColor"
|
||||
d="M80 264h40v40H80zm0 128h40v40H80zm128 0h40v40h-40zm-64-64h40v40h-40zm81.456-205.433l39.054-8.644l8.644 39.055l-39.054 8.644zm152.672 97.223l39.054-8.65l8.65 39.054l-39.054 8.65zm-76.324-48.649l39.053-8.65l8.65 39.053l-39.052 8.65z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 882 B |
12
frontend/src/assets/transaction.svg
Normal file
|
@ -0,0 +1,12 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
style="color: white;"
|
||||
>
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor">
|
||||
<path d="M4.58 8.607L2 8.454C3.849 3.704 9.158 1 14.333 2.344c5.513 1.433 8.788 6.918 7.314 12.25c-1.219 4.411-5.304 7.337-9.8 7.406"/>
|
||||
<path d="M12 22C6.5 22 2 17 2 11m11.604-1.278c-.352-.37-1.213-1.237-2.575-.62c-1.361.615-1.577 2.596.482 2.807c.93.095 1.537-.11 2.093.47c.556.582.659 2.198-.761 2.634s-2.341-.284-2.588-.509m1.653-6.484v.79m0 6.337v.873"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 639 B |
|
@ -71,6 +71,10 @@ a {
|
|||
@apply font-bold text-text-primary text-sm mb-2;
|
||||
}
|
||||
|
||||
.game-heading {
|
||||
@apply font-bold text-text-primary text-lg mb-2;
|
||||
}
|
||||
|
||||
.game-heading-xl {
|
||||
@apply font-bold text-text-primary text-xl mb-2;
|
||||
}
|
||||
|
|
3
justfile
|
@ -6,8 +6,7 @@ info:
|
|||
start:
|
||||
command -v concurrently &> /dev/null || bun add -g concurrently
|
||||
command -v watchexec &> /dev/null || brew install watchexec
|
||||
docker compose up -d
|
||||
conc -n "frontend,backend" "cd frontend && bun run start" "cd backend/ && watchexec -r -e java ./gradlew :bootRun"
|
||||
conc -n "frontend,backend,docker" "cd frontend && bun run start" "cd backend/ && watchexec -r -e java ./gradlew :bootRun" "docker compose up"
|
||||
|
||||
# Builds both the backend and frontend docker images (obv)
|
||||
build:
|
||||
|
|
132
projektdokumentation/.gitignore
vendored
Normal file
|
@ -0,0 +1,132 @@
|
|||
## Core latex/pdflatex auxiliary files:
|
||||
*.aux
|
||||
*.lof
|
||||
*.log
|
||||
*.lot
|
||||
*.fls
|
||||
*.out
|
||||
*.toc
|
||||
*.pdf
|
||||
## Intermediate documents:
|
||||
*.dvi
|
||||
*-converted-to.*
|
||||
# these rules might exclude image files for figures etc.
|
||||
# *.ps
|
||||
# *.eps
|
||||
# *.pdf
|
||||
|
||||
## Bibliography auxiliary files (bibtex/biblatex/biber):
|
||||
*.bbl
|
||||
*.bcf
|
||||
*.blg
|
||||
*-blx.aux
|
||||
*-blx.bib
|
||||
*.brf
|
||||
*.run.xml
|
||||
|
||||
## Build tool auxiliary files:
|
||||
*.fdb_latexmk
|
||||
*.synctex
|
||||
*.synctex.gz
|
||||
*.synctex.gz(busy)
|
||||
*.pdfsync
|
||||
|
||||
## Auxiliary and intermediate files from other packages:
|
||||
|
||||
# algorithms
|
||||
*.alg
|
||||
*.loa
|
||||
|
||||
# achemso
|
||||
acs-*.bib
|
||||
|
||||
# amsthm
|
||||
*.thm
|
||||
|
||||
# beamer
|
||||
*.nav
|
||||
*.snm
|
||||
*.vrb
|
||||
|
||||
#(e)ledmac/(e)ledpar
|
||||
*.end
|
||||
*.[1-9]
|
||||
*.[1-9][0-9]
|
||||
*.[1-9][0-9][0-9]
|
||||
*.[1-9]R
|
||||
*.[1-9][0-9]R
|
||||
*.[1-9][0-9][0-9]R
|
||||
*.eledsec[1-9]
|
||||
*.eledsec[1-9]R
|
||||
*.eledsec[1-9][0-9]
|
||||
*.eledsec[1-9][0-9]R
|
||||
*.eledsec[1-9][0-9][0-9]
|
||||
*.eledsec[1-9][0-9][0-9]R
|
||||
|
||||
# glossaries
|
||||
*.acn
|
||||
*.acr
|
||||
*.glg
|
||||
*.glo
|
||||
*.gls
|
||||
|
||||
# gnuplottex
|
||||
*-gnuplottex-*
|
||||
|
||||
# hyperref
|
||||
*.brf
|
||||
|
||||
# knitr
|
||||
*-concordance.tex
|
||||
*.tikz
|
||||
*-tikzDictionary
|
||||
|
||||
# listings
|
||||
*.lol
|
||||
|
||||
# makeidx
|
||||
*.idx
|
||||
*.ilg
|
||||
*.ind
|
||||
*.ist
|
||||
|
||||
# minitoc
|
||||
*.maf
|
||||
*.mtc
|
||||
*.mtc0
|
||||
|
||||
# minted
|
||||
_minted*
|
||||
*.pyg
|
||||
|
||||
# morewrites
|
||||
*.mw
|
||||
|
||||
# nomencl
|
||||
*.nlo
|
||||
|
||||
# sagetex
|
||||
*.sagetex.sage
|
||||
*.sagetex.py
|
||||
*.sagetex.scmd
|
||||
|
||||
# sympy
|
||||
*.sout
|
||||
*.sympy
|
||||
sympy-plots-for-*.tex/
|
||||
|
||||
# todonotes
|
||||
*.tdo
|
||||
|
||||
# xindy
|
||||
*.xdy
|
||||
|
||||
# WinEdt
|
||||
*.bak
|
||||
*.sav
|
||||
/tmp/
|
||||
|
||||
# idea
|
||||
.idea/
|
||||
|
||||
Projektdokumentation.syntex(busy)
|
17
projektdokumentation/.project
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>VorlageFachinformatiker</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>net.sourceforge.texlipse.builder.TexlipseBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>net.sourceforge.texlipse.builder.TexlipseNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
13
projektdokumentation/.texlipse
Normal file
|
@ -0,0 +1,13 @@
|
|||
#TeXlipse project settings
|
||||
#Fri Jan 27 09:15:16 CET 2012
|
||||
builderNum=2
|
||||
outputDir=
|
||||
makeIndSty=
|
||||
bibrefDir=
|
||||
outputFormat=pdf
|
||||
tempDir=tmp
|
||||
mainTexFile=Projektdokumentation.tex
|
||||
outputFile=Projektdokumentation.pdf
|
||||
langSpell=de
|
||||
markDer=true
|
||||
srcDir=
|
23
projektdokumentation/Abkuerzungen.tex
Normal file
|
@ -0,0 +1,23 @@
|
|||
% !TEX root = Projektdokumentation.tex
|
||||
|
||||
% Es werden nur die Abkürzungen aufgelistet, die mit \ac definiert und auch benutzt wurden.
|
||||
%
|
||||
% \acro{VERSIS}{Versicherungsinformationssystem\acroextra{ (Bestandsführungssystem)}}
|
||||
% Ergibt in der Liste: VERSIS Versicherungsinformationssystem (Bestandsführungssystem)
|
||||
% Im Text aber: \ac{VERSIS} -> Versicherungsinformationssystem (VERSIS)
|
||||
|
||||
% Hinweis: allgemein bekannte Abkürzungen wie z.B. bzw. u.a. müssen nicht ins Abkürzungsverzeichnis aufgenommen werden
|
||||
% Hinweis: allgemein bekannte IT-Begriffe wie Datenbank oder Programmiersprache müssen nicht erläutert werden,
|
||||
% aber ggfs. Fachbegriffe aus der Domäne des Prüflings (z.B. Versicherung)
|
||||
|
||||
% Die Option (in den eckigen Klammern) enthält das längste Label oder
|
||||
% einen Platzhalter der die Breite der linken Spalte bestimmt.
|
||||
\begin{acronym}[WWWWW]
|
||||
\acro{CI}{Continuous Integration}
|
||||
\acro{CI/CD}{Continuous Integration/Continuous Deployment}
|
||||
\acro{E2E}{End-to-End}
|
||||
\acro{API}{Application Programming Interface}
|
||||
\acro{JSON}{JavaScript Object Notation}
|
||||
\acro{HTTP}{Hypertext Transfer Protocol}
|
||||
\acro{JWT}{JSON Web Token}
|
||||
\end{acronym}
|
85
projektdokumentation/Allgemein/Befehle.tex
Normal file
|
@ -0,0 +1,85 @@
|
|||
% !TEX root = ../Projektdokumentation.tex
|
||||
|
||||
% Abkürzungen, ggfs. mit korrektem Leerraum
|
||||
\newcommand{\bs}{$\backslash$\xspace}
|
||||
\newcommand{\bspw}{bspw.\xspace}
|
||||
\newcommand{\bzw}{bzw.\xspace}
|
||||
\newcommand{\ca}{ca.\xspace}
|
||||
\newcommand{\dahe}{\mbox{d.\,h.}\xspace}
|
||||
\newcommand{\etc}{etc.\xspace}
|
||||
\newcommand{\eur}[1]{\mbox{#1\,\texteuro}\xspace}
|
||||
\newcommand{\evtl}{evtl.\xspace}
|
||||
\newcommand{\ggfs}{ggfs.\xspace}
|
||||
\newcommand{\Ggfs}{Ggfs.\xspace}
|
||||
\newcommand{\gqq}[1]{\glqq{}#1\grqq{}}
|
||||
\newcommand{\inkl}{inkl.\xspace}
|
||||
\newcommand{\insb}{insb.\xspace}
|
||||
\newcommand{\ua}{\mbox{u.\,a.}\xspace}
|
||||
\newcommand{\usw}{usw.\xspace}
|
||||
\newcommand{\Vgl}{Vgl.\xspace}
|
||||
\newcommand{\zB}{\mbox{z.\,B.}\xspace}
|
||||
|
||||
% Befehle für häufig anfallende Aufgaben
|
||||
\newcommand{\Abbildung}[1]{\autoref{fig:#1}}
|
||||
\newcommand{\Anhang}[1]{\appendixname{}~\ref{#1}: \nameref{#1} \vpageref{#1}}
|
||||
\newcommand{\includegraphicsKeepAspectRatio}[2]{\includegraphics[width=#2\textwidth,height=#2\textheight,keepaspectratio]{#1}}
|
||||
\newcommand{\Zitat}[2][\empty]{\ifthenelse{\equal{#1}{\empty}}{\citep{#2}}{\citep[#1]{#2}}}
|
||||
\newcommand{\Autor}[1]{\textsc{#1}} % zum Ausgeben von Autoren
|
||||
\newcommand{\itemd}[2]{\item{\textbf{#1}}\\{#2}} % erzeugt ein Listenelement mit fetter Überschrift
|
||||
|
||||
% fügt Tabellen aus einer TEX-Datei ein
|
||||
\newcommand{\tabelle}[3] % Parameter: caption, label, file
|
||||
{\begin{table}[htbp]
|
||||
\centering
|
||||
\singlespacing
|
||||
\input{Tabellen/#3}
|
||||
\caption{#1}
|
||||
\label{#2}
|
||||
\end{table}}
|
||||
|
||||
\newcommand{\tabelleAnhang}[1] % Parameter: file
|
||||
{\begin{center}
|
||||
\singlespacing
|
||||
\input{Tabellen/#1}
|
||||
\end{center}}
|
||||
|
||||
% einfaches Wechseln der Schrift, z.B.: \changefont{cmss}{sbc}{n}
|
||||
\newcommand{\changefont}[3]{\fontfamily{#1} \fontseries{#2} \fontshape{#3} \selectfont}
|
||||
|
||||
% Verwendung analog zu \includegraphics
|
||||
\newlength{\myx} % Variable zum Speichern der Bildbreite
|
||||
\newlength{\myy} % Variable zum Speichern der Bildhöhe
|
||||
\newcommand\includegraphicstotab[2][\relax]{%
|
||||
% Abspeichern der Bildabmessungen
|
||||
\settowidth{\myx}{\includegraphics[{#1}]{#2}}%
|
||||
\settoheight{\myy}{\includegraphics[{#1}]{#2}}%
|
||||
% das eigentliche Einfügen
|
||||
\parbox[c][1.1\myy][c]{\myx}{%
|
||||
\includegraphics[{#1}]{#2}}%
|
||||
}
|
||||
|
||||
\definecolor{AOBlau}{rgb}{0, 0.28, 0.56}
|
||||
|
||||
% verschiedene Befehle um Wörter semantisch auszuzeichnen ----------------------
|
||||
\newcommand{\Index}[2][\empty]{\ifthenelse{\equal{#1}{\empty}}{\index{#2}#2}{\index{#1}#2}}
|
||||
\newcommand{\Fachbegriff}[2][\empty]{\ifthenelse{\equal{#1}{\empty}}{\textit{\Index{#2}}}{\textit{\Index[#1]{#2}}}}
|
||||
\newcommand{\NeuerBegriff}[2][\empty]{\ifthenelse{\equal{#1}{\empty}}{\textbf{\Index{#2}}}{\textbf{\Index[#1]{#2}}}}
|
||||
|
||||
\newcommand{\Ausgabe}[1]{\texttt{#1}}
|
||||
\newcommand{\Eingabe}[1]{\texttt{#1}}
|
||||
\newcommand{\Code}[1]{\texttt{#1}}
|
||||
\newcommand{\Datei}[1]{\texttt{#1}}
|
||||
|
||||
\newcommand{\Assembly}[1]{\textsf{#1}}
|
||||
\newcommand{\Klasse}[1]{\textsf{#1}}
|
||||
\newcommand{\Methode}[1]{\textsf{#1}}
|
||||
\newcommand{\Attribut}[1]{\textsf{#1}}
|
||||
|
||||
\newcommand{\Datentyp}[1]{\textsf{#1}}
|
||||
\newcommand{\XMLElement}[1]{\textsf{#1}}
|
||||
\newcommand{\Webservice}[1]{\textsf{#1}}
|
||||
|
||||
\newcommand{\Refactoring}[1]{\Fachbegriff{#1}}
|
||||
\newcommand{\CodeSmell}[1]{\Fachbegriff{#1}}
|
||||
\newcommand{\Metrik}[1]{\Fachbegriff{#1}}
|
||||
\newcommand{\DesignPattern}[1]{\Fachbegriff{#1}}
|
196
projektdokumentation/Allgemein/Packages.tex
Normal file
|
@ -0,0 +1,196 @@
|
|||
% !TEX root = ../Projektdokumentation.tex
|
||||
|
||||
% Anpassung an Landessprache ---------------------------------------------------
|
||||
\usepackage{babel}
|
||||
|
||||
% Umlaute ----------------------------------------------------------------------
|
||||
% Umlaute/Sonderzeichen wie äüöß direkt im Quelltext verwenden (CodePage).
|
||||
% Erlaubt automatische Trennung von Worten mit Umlauten.
|
||||
% ------------------------------------------------------------------------------
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{textcomp} % Euro-Zeichen etc.
|
||||
|
||||
% Schrift ----------------------------------------------------------------------
|
||||
\usepackage{lmodern} % bessere Fonts
|
||||
\usepackage{relsize} % Schriftgröße relativ festlegen
|
||||
|
||||
% Tabellen ---------------------------------------------------------------------
|
||||
\PassOptionsToPackage{table}{xcolor}
|
||||
\usepackage{tabularx}
|
||||
% für lange Tabellen
|
||||
\usepackage{longtable}
|
||||
\usepackage{array}
|
||||
\usepackage{ragged2e}
|
||||
\usepackage{lscape}
|
||||
\newcolumntype{w}[1]{>{\raggedleft\hspace{0pt}}p{#1}} % Spaltendefinition rechtsbündig mit definierter Breite
|
||||
|
||||
% Grafiken ---------------------------------------------------------------------
|
||||
\usepackage[dvips,final]{graphicx} % Einbinden von JPG-Grafiken ermöglichen
|
||||
\usepackage{graphics} % keepaspectratio
|
||||
\usepackage{floatflt} % zum Umfließen von Bildern
|
||||
\graphicspath{{Bilder/}} % hier liegen die Bilder des Dokuments
|
||||
|
||||
% Sonstiges --------------------------------------------------------------------
|
||||
\usepackage[titles]{tocloft} % Inhaltsverzeichnis DIN 5008 gerecht einrücken
|
||||
|
||||
% Explicitly set German titles for ToC, LoF, LoT
|
||||
\renewcommand{\contentsname}{Inhaltsverzeichnis}
|
||||
\renewcommand{\listfigurename}{Abbildungsverzeichnis}
|
||||
\renewcommand{\listtablename}{Tabellenverzeichnis}
|
||||
|
||||
\usepackage{amsmath,amsfonts} % Befehle aus AMSTeX für mathematische Symbole
|
||||
\usepackage{enumitem} % anpassbare Enumerates/Itemizes
|
||||
\usepackage{xspace} % sorgt dafür, dass Leerzeichen hinter parameterlosen Makros nicht als Makroendezeichen interpretiert werden
|
||||
|
||||
\usepackage{makeidx} % für Index-Ausgabe mit \printindex
|
||||
\usepackage[printonlyused]{acronym} % es werden nur benutzte Definitionen aufgelistet
|
||||
|
||||
% Einfache Definition der Zeilenabstände und Seitenränder etc.
|
||||
\usepackage{setspace}
|
||||
\usepackage{geometry}
|
||||
|
||||
% Symbolverzeichnis
|
||||
\usepackage[intoc]{nomencl}
|
||||
\let\abbrev\nomenclature
|
||||
\renewcommand{\nomname}{Abkürzungsverzeichnis}
|
||||
\setlength{\nomlabelwidth}{.25\hsize}
|
||||
\renewcommand{\nomlabel}[1]{#1 \dotfill}
|
||||
\setlength{\nomitemsep}{-\parsep}
|
||||
|
||||
\usepackage{varioref} % Elegantere Verweise. „auf der nächsten Seite“
|
||||
\usepackage{url} % URL verlinken, lange URLs umbrechen etc.
|
||||
|
||||
\usepackage{chngcntr} % fortlaufendes Durchnummerieren der Fußnoten
|
||||
% \usepackage[perpage]{footmisc} % Alternative: Nummerierung der Fußnoten auf jeder Seite neu
|
||||
|
||||
\usepackage{ifthen} % bei der Definition eigener Befehle benötigt
|
||||
\usepackage{todonotes} % definiert u.a. die Befehle \todo und \listoftodos
|
||||
\usepackage[square]{natbib} % wichtig für korrekte Zitierweise
|
||||
|
||||
% PDF-Optionen -----------------------------------------------------------------
|
||||
\usepackage{pdfpages}
|
||||
\pdfminorversion=5 % erlaubt das Einfügen von pdf-Dateien bis Version 1.7, ohne eine Fehlermeldung zu werfen (keine Garantie für fehlerfreies Einbetten!)
|
||||
\usepackage[
|
||||
bookmarks,
|
||||
bookmarksnumbered,
|
||||
bookmarksopen=true,
|
||||
bookmarksopenlevel=1,
|
||||
colorlinks=true,
|
||||
% diese Farbdefinitionen zeichnen Links im PDF farblich aus
|
||||
linkcolor=AOBlau, % einfache interne Verknüpfungen
|
||||
anchorcolor=AOBlau,% Ankertext
|
||||
citecolor=AOBlau, % Verweise auf Literaturverzeichniseinträge im Text
|
||||
filecolor=AOBlau, % Verknüpfungen, die lokale Dateien öffnen
|
||||
menucolor=AOBlau, % Acrobat-Menüpunkte
|
||||
urlcolor=AOBlau,
|
||||
% diese Farbdefinitionen sollten für den Druck verwendet werden (alles schwarz)
|
||||
%linkcolor=black, % einfache interne Verknüpfungen
|
||||
%anchorcolor=black, % Ankertext
|
||||
%citecolor=black, % Verweise auf Literaturverzeichniseinträge im Text
|
||||
%filecolor=black, % Verknüpfungen, die lokale Dateien öffnen
|
||||
%menucolor=black, % Acrobat-Menüpunkte
|
||||
%urlcolor=black,
|
||||
%
|
||||
%backref, % Quellen werden zurück auf ihre Zitate verlinkt
|
||||
pdftex,
|
||||
plainpages=false, % zur korrekten Erstellung der Bookmarks
|
||||
pdfpagelabels=true, % zur korrekten Erstellung der Bookmarks
|
||||
hypertexnames=false, % zur korrekten Erstellung der Bookmarks
|
||||
linktocpage % Seitenzahlen anstatt Text im Inhaltsverzeichnis verlinken
|
||||
]{hyperref}
|
||||
% Befehle, die Umlaute ausgeben, führen zu Fehlern, wenn sie hyperref als Optionen übergeben werden
|
||||
\hypersetup{
|
||||
pdftitle={\titel -- \untertitel},
|
||||
pdfauthor={\autorName},
|
||||
pdfcreator={\autorName},
|
||||
pdfsubject={\titel -- \untertitel},
|
||||
pdfkeywords={\titel -- \untertitel},
|
||||
}
|
||||
|
||||
|
||||
% zum Einbinden von Programmcode -----------------------------------------------
|
||||
\usepackage{listings}
|
||||
% Explicitly set German title for LoL
|
||||
\renewcommand{\lstlistlistingname}{Codeverzeichnis} % For list of listings
|
||||
\usepackage{xcolor}
|
||||
\definecolor{hellgelb}{rgb}{1,1,0.9}
|
||||
\definecolor{colKeys}{rgb}{0,0,1}
|
||||
\definecolor{colIdentifier}{rgb}{0,0,0}
|
||||
\definecolor{colComments}{rgb}{0,0.5,0}
|
||||
\definecolor{colString}{rgb}{1,0,0}
|
||||
\lstset{
|
||||
float=hbp,
|
||||
basicstyle=\footnotesize,
|
||||
identifierstyle=\color{colIdentifier},
|
||||
keywordstyle=\color{colKeys},
|
||||
stringstyle=\color{colString},
|
||||
commentstyle=\color{colComments},
|
||||
backgroundcolor=\color{hellgelb},
|
||||
columns=flexible,
|
||||
tabsize=2,
|
||||
frame=single,
|
||||
extendedchars=true,
|
||||
showspaces=false,
|
||||
showstringspaces=false,
|
||||
numbers=left,
|
||||
numberstyle=\tiny,
|
||||
breaklines=true,
|
||||
breakautoindent=true,
|
||||
captionpos=b,
|
||||
}
|
||||
\lstdefinelanguage{cs}{
|
||||
sensitive=false,
|
||||
morecomment=[l]{//},
|
||||
morecomment=[s]{/*}{*/},
|
||||
morestring=[b]",
|
||||
morekeywords={
|
||||
abstract,event,new,struct,as,explicit,null,switch
|
||||
base,extern,object,this,bool,false,operator,throw,
|
||||
break,finally,out,true,byte,fixed,override,try,
|
||||
case,float,params,typeof,catch,for,private,uint,
|
||||
char,foreach,protected,ulong,checked,goto,public,unchecked,
|
||||
class,if,readonly,unsafe,const,implicit,ref,ushort,
|
||||
continue,in,return,using,decimal,int,sbyte,virtual,
|
||||
default,interface,sealed,volatile,delegate,internal,short,void,
|
||||
do,is,sizeof,while,double,lock,stackalloc,
|
||||
else,long,static,enum,namespace,string},
|
||||
}
|
||||
\lstdefinelanguage{natural}{
|
||||
sensitive=false,
|
||||
morecomment=[l]{/*},
|
||||
morestring=[b]",
|
||||
morestring=[b]',
|
||||
alsodigit={-,*},
|
||||
morekeywords={
|
||||
DEFINE,DATA,LOCAL,END-DEFINE,WRITE,CALLNAT,PARAMETER,USING,
|
||||
IF,NOT,END-IF,ON,*ERROR-NR,ERROR,END-ERROR,ESCAPE,ROUTINE,
|
||||
PERFORM,SUBROUTINE,END-SUBROUTINE,CONST,END-FOR,END,FOR,RESIZE,
|
||||
ARRAY,TO,BY,VALUE,RESET,COMPRESS,INTO,EQ},
|
||||
}
|
||||
\lstdefinelanguage{php}{
|
||||
sensitive=false,
|
||||
morecomment=[l]{/*},
|
||||
morestring=[b]",
|
||||
morestring=[b]',
|
||||
alsodigit={-,*},
|
||||
morekeywords={
|
||||
abstract,and,array,as,break,case,catch,cfunction,class,clone,const,
|
||||
continue,declare,default,do,else,elseif,enddeclare,endfor,endforeach,
|
||||
endif,endswitch,endwhile,extends,final,for,foreach,function,global,
|
||||
goto,if,implements,interface,instanceof,namespace,new,old_function,or,
|
||||
private,protected,public,static,switch,throw,try,use,var,while,xor
|
||||
die,echo,empty,exit,eval,include,include_once,isset,list,require,
|
||||
require_once,return,print,unset},
|
||||
}
|
||||
\lstdefinelanguage{json}{
|
||||
basicstyle=\ttfamily\small,
|
||||
showstringspaces=false,
|
||||
breaklines=true,
|
||||
commentstyle=\color{green!50!black},
|
||||
stringstyle=\color{red},
|
||||
keywords={true, false, null},
|
||||
keywordstyle=\color{blue}\bfseries,
|
||||
morestring=[s]{"}{"},
|
||||
morecomment=[l]{//},
|
||||
morecomment=[s]{/*}{*/},
|
||||
}
|
83
projektdokumentation/Allgemein/Seitenstil.tex
Normal file
|
@ -0,0 +1,83 @@
|
|||
% !TEX root = ../Projektdokumentation.tex
|
||||
|
||||
% Seitenränder -----------------------------------------------------------------
|
||||
\setlength{\topskip}{\ht\strutbox} % behebt Warnung von geometry
|
||||
\geometry{a4paper,left=25mm,right=25mm,top=33mm,bottom=33mm,head=53.14769pt}
|
||||
\deffootnote{1em}{1em}{\textsuperscript{\thefootnotemark}\fontsize{9pt}{11pt}\selectfont}
|
||||
|
||||
\usepackage[
|
||||
automark, % Kapitelangaben in Kopfzeile automatisch erstellen
|
||||
headsepline, % Trennlinie unter Kopfzeile
|
||||
ilines % Trennlinie linksbündig ausrichten
|
||||
]{scrlayer-scrpage}
|
||||
|
||||
% Kopf- und Fußzeilen ----------------------------------------------------------
|
||||
\pagestyle{scrheadings}
|
||||
% chapterpagestyle gibt es nicht in scrartcl
|
||||
%\renewcommand{\chapterpagestyle}{scrheadings}
|
||||
\clearpairofpagestyles
|
||||
|
||||
% Kopfzeile
|
||||
\renewcommand{\headfont}{\normalfont} % Schriftform der Kopfzeile
|
||||
\ihead{\large{\textsc{\titel}}\\ \small{\untertitel} \\[2ex] \textit{\headmark}}
|
||||
\chead{}
|
||||
\ohead{\includegraphics[scale=0.09]{\betriebLogo}}
|
||||
%\setheadwidth[0pt]{textwithmarginpar} % Kopfzeile über den Text hinaus verbreitern (falls Logo den Text überdeckt)
|
||||
|
||||
% Fußzeile
|
||||
\cfoot{}
|
||||
\ofoot{\pagemark}
|
||||
|
||||
|
||||
% Überschriften nach DIN 5008 in einer Fluchtlinie
|
||||
% ------------------------------------------------------------------------------
|
||||
|
||||
% Abstand zwischen Nummerierung und Überschrift definieren
|
||||
% > Schön wäre hier die dynamische Berechnung des Abstandes in Abhängigkeit
|
||||
% > der Verschachtelungstiefe des Inhaltsverzeichnisses
|
||||
\newcommand{\headingSpace}{1.5cm}
|
||||
|
||||
% Abschnittsüberschriften im selben Stil wie beim Inhaltsverzeichnis einrücken
|
||||
\renewcommand*{\othersectionlevelsformat}[3]{
|
||||
\makebox[\headingSpace][l]{#3\autodot}
|
||||
}
|
||||
|
||||
% Für die Einrückung wird das Paket tocloft benötigt
|
||||
%\cftsetindents{chapter}{0.0cm}{\headingSpace}
|
||||
\cftsetindents{section}{0.0cm}{\headingSpace}
|
||||
\cftsetindents{subsection}{0.0cm}{\headingSpace}
|
||||
\cftsetindents{subsubsection}{0.0cm}{\headingSpace}
|
||||
\cftsetindents{figure}{0.0cm}{\headingSpace}
|
||||
\cftsetindents{table}{0.0cm}{\headingSpace}
|
||||
|
||||
|
||||
% Allgemeines
|
||||
% ------------------------------------------------------------------------------
|
||||
|
||||
\setstretch{1.15} % Zeilenabstand 1.15 Zeilen
|
||||
\frenchspacing % erzeugt ein wenig mehr Platz hinter einem Punkt
|
||||
|
||||
\renewcommand{\rmdefault}{phv} % Arial als Std Schriftart
|
||||
\renewcommand{\sfdefault}{phv}
|
||||
|
||||
% Schusterjungen und Hurenkinder vermeiden
|
||||
\clubpenalty = 10000
|
||||
\widowpenalty = 10000
|
||||
\displaywidowpenalty = 10000
|
||||
|
||||
% Quellcode-Ausgabe formatieren
|
||||
\lstset{numbers=left, numberstyle=\tiny, numbersep=5pt, breaklines=true}
|
||||
\lstset{emph={square}, emphstyle=\color{red}, emph={[2]root,base}, emphstyle={[2]\color{blue}}}
|
||||
|
||||
\counterwithout{footnote}{section} % Fußnoten fortlaufend durchnummerieren
|
||||
\setcounter{tocdepth}{3} % im Inhaltsverzeichnis werden die Kapitel bis zum Level der subsubsection übernommen
|
||||
\setcounter{secnumdepth}{3} % Kapitel bis zum Level der subsubsection werden nummeriert
|
||||
|
||||
% Aufzählungen anpassen
|
||||
\renewcommand{\labelenumi}{\arabic{enumi}.}
|
||||
\renewcommand{\labelenumii}{\arabic{enumi}.\arabic{enumii}.}
|
||||
\renewcommand{\labelenumiii}{\arabic{enumi}.\arabic{enumii}.\arabic{enumiii}}
|
||||
|
||||
% Tabellenfärbung:
|
||||
\definecolor{heading}{rgb}{0.64,0.78,0.86}
|
||||
\definecolor{odd}{rgb}{0.9,0.9,0.9}
|
3
projektdokumentation/Allgemein/Silbentrennung.tex
Normal file
|
@ -0,0 +1,3 @@
|
|||
% Trennvorschläge im Text werden mit \" angegeben
|
||||
% untrennbare Wörter und Ausnahmen von der normalen Trennung können in dieser
|
||||
% Datei mittels \hyphenation definiert werden
|
2234
projektdokumentation/Allgemein/natdin.bst
Normal file
35
projektdokumentation/Anhang.tex
Normal file
|
@ -0,0 +1,35 @@
|
|||
% !TEX root = Projektdokumentation.tex
|
||||
\section{Anhang}
|
||||
|
||||
\subsection{Implementierungsbeispiele}
|
||||
\label{app:CodeSchichten}
|
||||
|
||||
\subsubsection{Frontend-Schicht: Angular Component}
|
||||
\label{app:FrontendComponent}
|
||||
\lstinputlisting[language=C, caption={Angular TypeScript Component - Coinflip Game}]{Listings/CoinflipComponent.ts}
|
||||
|
||||
\clearpage
|
||||
|
||||
\subsubsection{Controller-Schicht: Spring Boot REST Controller}
|
||||
\label{app:ControllerSchicht}
|
||||
\lstinputlisting[language=java, caption={Spring Boot REST Controller - Coinflip}]{Listings/CoinflipController.java}
|
||||
|
||||
\clearpage
|
||||
|
||||
\subsubsection{Service-Schicht: Business Logic}
|
||||
\label{app:ServiceSchicht}
|
||||
\lstinputlisting[language=java, caption={Service-Klasse mit Geschäftslogik - Coinflip}]{Listings/CoinflipService.java}
|
||||
|
||||
\clearpage
|
||||
|
||||
\subsubsection{Persistierung-Schicht: JPA Entity}
|
||||
\label{app:PersistierungSchicht}
|
||||
\lstinputlisting[language=java, caption={JPA Entity - Benutzer}]{Listings/UserEntity.java}
|
||||
|
||||
\clearpage
|
||||
|
||||
\subsubsection{Konfiguration: Application Properties}
|
||||
\label{app:Konfiguration}
|
||||
\lstinputlisting[caption={Spring Boot Anwendungskonfiguration}]{Listings/application.properties}
|
||||
|
||||
\clearpage
|
15
projektdokumentation/Anhang/AnhangBenutzerDoku.tex
Normal file
|
@ -0,0 +1,15 @@
|
|||
\subsection{Benutzerdokumentation}
|
||||
\label{app:BenutzerDoku}
|
||||
Ausschnitt aus der Benutzerdokumentation:
|
||||
|
||||
\begin{table}[htb]
|
||||
\begin{tabularx}{\textwidth}{cXX}
|
||||
\rowcolor{heading}\textbf{Symbol} & \textbf{Bedeutung global} & \textbf{Bedeutung einzeln} \\
|
||||
\includegraphicstotab[]{weather-clear.png} & Alle Module weisen den gleichen Stand auf. & Das Modul ist auf dem gleichen Stand wie das Modul auf der vorherigen Umgebung. \\
|
||||
\rowcolor{odd}\includegraphicstotab[]{weather-clear-night.png} & Es existieren keine Module (fachlich nicht möglich). & Weder auf der aktuellen noch auf der vorherigen Umgebung sind Module angelegt. Es kann also auch nichts übertragen werden. \\
|
||||
\includegraphicstotab[]{weather-few-clouds-night.png} & Ein Modul muss durch das Übertragen von der vorherigen Umgebung erstellt werden. & Das Modul der vorherigen Umgebung kann übertragen werden, auf dieser Umgebung ist noch kein Modul vorhanden. \\
|
||||
\rowcolor{odd}\includegraphicstotab[]{weather-few-clouds.png} & Auf einer vorherigen Umgebung gibt es ein Modul, welches übertragen werden kann, um das nächste zu aktualisieren. & Das Modul der vorherigen Umgebung kann übertragen werden um dieses zu aktualisieren. \\
|
||||
\includegraphicstotab[]{weather-storm.png} & Ein Modul auf einer Umgebung wurde entgegen des Entwicklungsprozesses gespeichert. & Das aktuelle Modul ist neuer als das Modul auf der vorherigen Umgebung oder die vorherige Umgebung wurde übersprungen. \\
|
||||
\end{tabularx}
|
||||
\end{table}
|
||||
|
7
projektdokumentation/Anhang/AnhangDoc.tex
Normal file
|
@ -0,0 +1,7 @@
|
|||
\subsection{Entwicklerdokumentation}
|
||||
\label{app:Doc}
|
||||
\begin{center}
|
||||
\includegraphics[page=1, width=0.9\textwidth]{doc.pdf}
|
||||
|
||||
\includegraphics[page=2, width=0.9\textwidth]{doc.pdf}
|
||||
\end{center}
|
19
projektdokumentation/Anhang/AnhangEntwuerfe.tex
Normal file
|
@ -0,0 +1,19 @@
|
|||
\subsection{Oberflächenentwürfe}
|
||||
\label{app:Entwuerfe}
|
||||
\begin{figure}[htb]
|
||||
\centering
|
||||
\includegraphicsKeepAspectRatio{MockupModules.pdf}{0.7}
|
||||
\caption{Liste der Module mit Filtermöglichkeiten}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[htb]
|
||||
\centering
|
||||
\includegraphicsKeepAspectRatio{MockupModul.pdf}{0.7}
|
||||
\caption{Anzeige der Übersichtsseite einzelner Module}
|
||||
\end{figure}
|
||||
|
||||
\begin{figure}[htb]
|
||||
\centering
|
||||
\includegraphicsKeepAspectRatio{MockupTag.pdf}{0.7}
|
||||
\caption{Anzeige und Filterung der Module nach Tags}
|
||||
\end{figure}
|
22
projektdokumentation/Anhang/AnhangLastenheft.tex
Normal file
|
@ -0,0 +1,22 @@
|
|||
\subsection{Lastenheft (Auszug)}
|
||||
\label{app:Lastenheft}
|
||||
Im folgenden Auszug des Lastenheftes werden die Anforderungen definiert, die an die neu entwickelte Anwendung gestellt werden.
|
||||
|
||||
Die Anwendung muss folgende Anforderungen erfüllen:
|
||||
\begin{enumerate}[itemsep=0em,partopsep=0em,parsep=0em,topsep=0em]
|
||||
\item Verarbeitung der empfangenen Daten von \ac{IX}
|
||||
\begin{enumerate}
|
||||
\item Die Anwendung muss Produkt- und Nutzerdaten von der \ac{IX}-\ac{API} abrufen können.
|
||||
\item Die geholten Daten sollen temporär zwischengespeichert werden, um den Import zu \ac{M2} von der Datenbeschaffung abzukapseln
|
||||
\item Die gespeicherten Daten werden getrennt von der \ac{IX}-\ac{API} an die \ac{M2} Import-Funktionen gegeben und so in das \ac{M2} System integriert
|
||||
\end{enumerate}
|
||||
\item Sonstige Anforderungen
|
||||
\begin{enumerate}
|
||||
\item Die Anwendung muss ohne das Installieren einer zusätzlichen Software über das Terminal erreichbar sein.
|
||||
\item Die Import der Daten muss jede Nacht \bzw nach jedem Fehlschlag automatisch aktualisiert werden.
|
||||
\item Die Anwendung soll jederzeit erreichbar sein.
|
||||
\item Da sich die Entwickler auf die Anwendung verlassen, muss diese korrekte Daten liefern und darf keinen Interpretationsspielraum lassen.
|
||||
\item Die Anwendung muss so flexibel sein, dass sie bei Änderungen im Entwicklungsprozess einfach angepasst werden kann.
|
||||
\end{enumerate}
|
||||
\end{enumerate}
|
||||
|
52
projektdokumentation/Anhang/AnhangPflichtenheft.tex
Normal file
|
@ -0,0 +1,52 @@
|
|||
\subsection{Pflichtenheft (Auszug)}
|
||||
\label{app:Pflichtenheft}
|
||||
|
||||
\subsubsection*{Zielbestimmung}
|
||||
|
||||
\begin{enumerate}[itemsep=0em,partopsep=0em,parsep=0em,topsep=0em]
|
||||
\item Musskriterien % Wikipedia: für das Produkt unabdingbare Leistungen, die in jedem Fall erfüllt werden müssen
|
||||
\begin{enumerate}
|
||||
\item Modul-Liste: Zeigt eine filterbare Liste der Module mit den dazugehörigen Kerninformationen sowie Symbolen zur Einhaltung des Entwicklungsprozesses an
|
||||
\begin{itemize}
|
||||
\item In der Liste wird der Name, die Bibliothek und Daten zum Source und Kompilat eines Moduls angezeigt.
|
||||
\item Ebenfalls wird der Status des Moduls hinsichtlich Source und Kompilat angezeigt. Dazu gibt es unterschiedliche Status-Zeichen, welche symbolisieren in wie weit der Entwicklungsprozess eingehalten wurde \bzw welche Schritte als nächstes getan werden müssen. So gibt es \zB Zeichen für das Einhalten oder Verletzen des Prozesses oder den Hinweis auf den nächsten zu tätigenden Schritt.
|
||||
\item Weiterhin werden die Benutzer und Zeitpunkte der aktuellen Version der Sourcen und Kompilate angezeigt. Dazu kann vorher ausgewählt werden, von welcher Umgebung diese Daten gelesen werden sollen.
|
||||
\item Es kann eine Filterung nach allen angezeigten Daten vorgenommen werden. Die Daten zu den Sourcen sind historisiert. Durch die Filterung ist es möglich, auch Module zu finden, die in der Zwischenzeit schon von einem anderen Benutzer editiert wurden.
|
||||
\end{itemize}
|
||||
\item Tag-Liste: Bietet die Möglichkeit die Module anhand von Tags zu filtern.
|
||||
\begin{itemize}
|
||||
\item Es sollen die Tags angezeigt werden, nach denen bereits gefiltert wird und die, die noch der Filterung hinzugefügt werden könnten, ohne dass die Ergebnisliste leer wird.
|
||||
\item Zusätzlich sollen die Module angezeigt werden, die den Filterkriterien entsprechen. Sollten die Filterkriterien leer sein, werden nur die Module angezeigt, welche mit einem Tag versehen sind.
|
||||
\end{itemize}
|
||||
\item Import der Moduldaten aus einer bereitgestellten \acs{CSV}-Datei
|
||||
\begin{itemize}
|
||||
\item Es wird täglich eine Datei mit den Daten der aktuellen Module erstellt. Diese Datei wird (durch einen Cronjob) automatisch nachts importiert.
|
||||
\item Dabei wird für jedes importierte Modul ein Zeitstempel aktualisiert, damit festgestellt werden kann, wenn ein Modul gelöscht wurde.
|
||||
\item Die Datei enthält die Namen der Umgebung, der Bibliothek und des Moduls, den Programmtyp, den Benutzer und Zeitpunkt des Sourcecodes sowie des Kompilats und den Hash des Sourcecodes.
|
||||
\item Sollte sich ein Modul verändert haben, werden die entsprechenden Daten in der Datenbank aktualisiert. Die Veränderungen am Source werden dabei aber nicht ersetzt, sondern historisiert.
|
||||
\end{itemize}
|
||||
\item Import der Informationen aus \ac{SVN}. Durch einen \gqq{post-commit-hook} wird nach jedem Einchecken eines Moduls ein \acs{PHP}-Script auf der Konsole aufgerufen, welches die Informationen, die vom \ac{SVN}-Kommandozeilentool geliefert werden, an \acs{NatInfo} übergibt.
|
||||
\item Parsen der Sourcen
|
||||
\begin{itemize}
|
||||
\item Die Sourcen der Entwicklungsumgebung werden nach Tags, Links zu Artikeln im Wiki und Programmbeschreibungen durchsucht.
|
||||
\item Diese Daten werden dann entsprechend angelegt, aktualisiert oder nicht mehr gesetzte Tags/Wikiartikel entfernt.
|
||||
\end{itemize}
|
||||
\item Sonstiges
|
||||
\begin{itemize}
|
||||
\item Das Programm läuft als Webanwendung im Intranet.
|
||||
\item Die Anwendung soll möglichst leicht erweiterbar sein und auch von anderen Entwicklungsprozessen ausgehen können.
|
||||
\item Eine Konfiguration soll möglichst in zentralen Konfigurationsdateien erfolgen.
|
||||
\end{itemize}
|
||||
\end{enumerate}
|
||||
\end{enumerate}
|
||||
|
||||
\subsubsection*{Produkteinsatz}
|
||||
|
||||
\begin{enumerate}[itemsep=0em,partopsep=0em,parsep=0em,topsep=0em]
|
||||
\item{Anwendungsbereiche\\
|
||||
Die Webanwendung dient als Anlaufstelle für die Entwicklung. Dort sind alle Informationen für die Module an einer Stelle gesammelt. Vorher getrennte Anwendungen werden ersetzt \bzw verlinkt.}
|
||||
\item{Zielgruppen\\
|
||||
\NI wird lediglich von den \ac{Natural}-Entwicklern in der EDV-Abteilung genutzt.}
|
||||
\item{Betriebsbedingungen\\ % Wikipedia: physikalische Umgebung des Systems, tägliche Betriebszeit, ständige Beobachtung des Systems durch Bediener oder unbeaufsichtigter Betrieb
|
||||
Die nötigen Betriebsbedingungen, also der Webserver, die Datenbank, die Versionsverwaltung, das Wiki und der nächtliche Export sind bereits vorhanden und konfiguriert. Durch einen täglichen Cronjob werden entsprechende Daten aktualisiert, die Webanwendung ist jederzeit aus dem Intranet heraus erreichbar.}
|
||||
\end{enumerate}
|
28
projektdokumentation/Anhang/AnhangRessourcen.tex
Normal file
|
@ -0,0 +1,28 @@
|
|||
\subsection{Verwendete Ressourcen}
|
||||
\label{app:Ressourcen}
|
||||
Hardware
|
||||
\begin{itemize}
|
||||
\item Büroarbeitsplatz (Schreibtisch, ergonomischer Stuhl)
|
||||
\item Fujitsu Lifebook U-Series - Notebook
|
||||
\item Peripheriegeräte für Notebook (Tastatur, Maus)
|
||||
\end{itemize}
|
||||
|
||||
Software
|
||||
\begin{itemize}
|
||||
\item Debian Derivat Ubuntu 18.04 - Betriebssystem
|
||||
\item JetBrains PHPStorm - Entwicklungsumgebung \ac{PHP}
|
||||
\item git - Verteilte Versionsverwaltung
|
||||
\item Docker - Open Source Management von virtuellen Maschinen (Containerbasiert)
|
||||
\item mySQL - Open Source relationelles Datenbank Management System
|
||||
\item Composer - Open Source Package Management System auf Anwendungsebene für \ac{PHP}
|
||||
\item Magento 2 Community Edition - Open Source \ac{eCommerce} Platform
|
||||
\item TeXify IDEA - Open Source Plugin für PHPStorm als Distribution des Textsatzsystems TeX
|
||||
\item PHPUnit - Framework zur Durchführung von Unit-Tests
|
||||
\item LaTeX-Vorlage zur IHK-Projektdokumentation für Fachinformatiker Anwendungsentwicklung von Stefan Macke - \url{http://fiae.link/LaTeXVorlageFIAE}
|
||||
\end{itemize}
|
||||
|
||||
Personal
|
||||
\begin{itemize}
|
||||
\item Mitarbeiter Softwareentwicklung der \ac{NSD} - Festlegung der Anforderungen, Abnahme des Projektes \& Review des Codes
|
||||
\item Auszubildender Fachinformatiker als Entwickler - Umsetzung des Projektes
|
||||
\end{itemize}
|
14
projektdokumentation/Anhang/AnhangScreenshots.tex
Normal file
|
@ -0,0 +1,14 @@
|
|||
\subsection{Screenshots der Anwendung}
|
||||
\label{Screenshots}
|
||||
\begin{figure}[htb]
|
||||
\centering
|
||||
\includegraphicsKeepAspectRatio{tagliste.pdf}{1}
|
||||
\caption{Anzeige und Filterung der Module nach Tags}
|
||||
\end{figure}
|
||||
\clearpage
|
||||
\begin{figure}[htb]
|
||||
\centering
|
||||
\includegraphicsKeepAspectRatio{modulliste.pdf}{1}
|
||||
\caption{Liste der Module mit Filtermöglichkeiten}
|
||||
\end{figure}
|
||||
\clearpage
|
9
projektdokumentation/Anhang/AnhangTest.tex
Normal file
|
@ -0,0 +1,9 @@
|
|||
\subsection{Testfall und sein Aufruf auf der Konsole}
|
||||
\label{app:Test}
|
||||
\lstinputlisting[language=php, caption={Testfall in PHP}]{Listings/tests.php}
|
||||
\clearpage
|
||||
\begin{figure}[htb]
|
||||
\centering
|
||||
\includegraphicsKeepAspectRatio{testcase.jpg}{1}
|
||||
\caption{Aufruf des Testfalls auf der Konsole}
|
||||
\end{figure}
|
4
projektdokumentation/Befehle.tex
Normal file
|
@ -0,0 +1,4 @@
|
|||
% Abkürzungen
|
||||
\newcommand{\Versis}{\textsc{Versis}\xspace}
|
||||
\newcommand{\NI}{NatInfo\xspace}
|
||||
\newcommand{\AO}{\textsc{Alte Oldenburger} Krankenversicherung\xspace}
|
92
projektdokumentation/Bibliographie.bib
Normal file
|
@ -0,0 +1,92 @@
|
|||
% This file was created with JabRef 2.4.2.
|
||||
% Encoding: UTF8
|
||||
|
||||
@book{Rohrer2011,
|
||||
address = {Solingen},
|
||||
edition = {5.},
|
||||
title = {Clevere Tipps für die Projektarbeit - {IT-Berufe:} Abschlussprüfung Teil A},
|
||||
isbn = {3882347538},
|
||||
url = {http://fiae.link/ClevereTippsFuerDieProjektarbeit},
|
||||
publisher = {{U-Form-Verlag}},
|
||||
author = {Rohrer, Anselm and Sedlacek, Ramona},
|
||||
year = {2011}
|
||||
}
|
||||
|
||||
@misc{Bundesgesetzblatt48,
|
||||
title = {Verordnung über die Berufsausbildung im Bereich der Informations- und Telekommunikationstechnik},
|
||||
url = {http://fiae.link/VerordnungITBerufe},
|
||||
author = {{Regierung der Bundesrepublik Deutschland}},
|
||||
month = jul,
|
||||
year = {1997},
|
||||
pages = {1741--1799}
|
||||
}
|
||||
|
||||
@techreport{BMBF2000,
|
||||
address = {Bonn},
|
||||
type = {Abschlussbericht},
|
||||
title = {Umsetzungshilfen für die neue Prüfungsstruktur der {IT-Berufe}},
|
||||
shorttitle = {{BMBF2000}},
|
||||
url = {http://fiae.link/UmsetzungshilfenITBerufe},
|
||||
author = {{Bundesministerium für Bildung und Forschung}},
|
||||
institution = {{Bundesministerium für Bildung und Forschung}},
|
||||
month = jul,
|
||||
year = {2000},
|
||||
pages = {476}
|
||||
}
|
||||
|
||||
@techreport{Grashorn2010,
|
||||
address = {Vechta},
|
||||
type = {Dokumentation zur Projektarbeit},
|
||||
title = {Entwicklung von NatInfo -- Webbasiertes Tool zur Unterstützung der Entwickler},
|
||||
author = {Dirk Grashorn},
|
||||
institution = {{Alte Oldenburger Krankenversicherung AG}},
|
||||
month = apr,
|
||||
year = {2010},
|
||||
}
|
||||
|
||||
@misc{MerkblattIHK,
|
||||
address = {Oldenburg},
|
||||
title = {Merkblatt zur Abschlussprüfung der IT-Berufe},
|
||||
author = {{IHK Oldenburg}},
|
||||
institution = {{Oldenburgische Industrie- und Handelskammer}},
|
||||
month = may,
|
||||
year = {2006},
|
||||
url = {http://fiae.link/MerkblattDokuOldenburg},
|
||||
}
|
||||
|
||||
@misc{BewertungsmatrikIHK,
|
||||
address = {Darmstadt},
|
||||
title = {Bewertungsmatrix für Fachinformatiker/innen Anwendungsentwicklung},
|
||||
author = {{IHK Darmstadt}},
|
||||
institution = {{IHK Darmstadt Rhein Main Neckar}},
|
||||
month = mar,
|
||||
year = {2011},
|
||||
url = {http://fiae.link/BewertungsmatrixDokuDarmstadt},
|
||||
}
|
||||
|
||||
@misc{ISO9126,
|
||||
title = {{Software-Engineering} – Qualität von {Software-Produkten} – Teil 1: Qualitätsmodell},
|
||||
author = {{ISO/IEC 9126-1}},
|
||||
month = jun,
|
||||
year = {2001}
|
||||
}
|
||||
|
||||
@BOOKLET{phpDoc,
|
||||
title = {{phpDocumentor-Website}},
|
||||
author = {{phpdoc.org}},
|
||||
lastchecked = {20.04.2010},
|
||||
year = {2010},
|
||||
owner = {grashorn},
|
||||
timestamp = {2010.04.22},
|
||||
url = {http://www.phpdoc.org/}
|
||||
}
|
||||
|
||||
@BOOKLET{Symfony,
|
||||
title = {Symfony - Open-Source PHP Web Framework},
|
||||
author = {{Sensio Labs}},
|
||||
lastchecked = {20.04.2010},
|
||||
year = {2010},
|
||||
owner = {grashorn},
|
||||
timestamp = {2010.04.22},
|
||||
url = {http://www.symfony-project.org/}
|
||||
}
|
BIN
projektdokumentation/Bilder/LogoBetrieb.pdf
Normal file
BIN
projektdokumentation/Bilder/component-diagram.drawio.png
Normal file
After Width: | Height: | Size: 261 KiB |
BIN
projektdokumentation/Bilder/container-diagram.drawio.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
projektdokumentation/Bilder/hitec-logo.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
projektdokumentation/Bilder/login.png
Executable file
After Width: | Height: | Size: 11 KiB |
BIN
projektdokumentation/Bilder/oauth.png
Executable file
After Width: | Height: | Size: 9.4 KiB |
27
projektdokumentation/Deckblatt.tex
Normal file
|
@ -0,0 +1,27 @@
|
|||
% !TEX root = Projektdokumentation.tex
|
||||
\begin{titlepage}
|
||||
|
||||
\begin{center}
|
||||
\includegraphics[scale=0.25]{hitec-logo.png}\\[1ex]
|
||||
|
||||
\Large{\ausbildungsberuf}\\
|
||||
\LARGE{\betreff}\\[4ex]
|
||||
|
||||
\huge{\textbf{\titel}}\\[1.5ex]
|
||||
\Large{\textbf{\untertitel}}\\[4ex]
|
||||
|
||||
\normalsize
|
||||
Projektzeitraum: \projektZeitraum\\[3em]
|
||||
\textbf{Projektteilnehmer:}\\
|
||||
Constantin Simonis\\
|
||||
Phan Huy Tran\\
|
||||
Jan-Marlon Leibl\\
|
||||
Jan Klattenhoff\\
|
||||
Lea Ziemke\\
|
||||
|
||||
\textbf{Projektaufsicht:}\\
|
||||
Katrin Deeken\\
|
||||
Bernd Heidemann\\
|
||||
\end{center}
|
||||
|
||||
\end{titlepage}
|
16
projektdokumentation/Inhalt.tex
Normal file
|
@ -0,0 +1,16 @@
|
|||
% !TEX root = Projektdokumentation.tex
|
||||
\input{Inhalt/Einleitung}
|
||||
\input{Inhalt/Vorbereitung.tex}
|
||||
\input{Inhalt/Projektarchitektur}
|
||||
\input{Inhalt/CI}
|
||||
\input{Inhalt/Auth.tex}
|
||||
\input{Inhalt/Dice.tex}
|
||||
\input{Inhalt/Slots.tex}
|
||||
\input{Inhalt/Coinflip.tex}
|
||||
\input{Inhalt/Blackjack.tex}
|
||||
\input{Inhalt/Lootboxes.tex}
|
||||
|
||||
\input{Inhalt/Deployment.tex}
|
||||
|
||||
\input{Inhalt/Wirtschaftlichebetrachtung.tex}
|
||||
\input{Inhalt/Abschluss.tex}
|
12
projektdokumentation/Inhalt/Abschluss.tex
Normal file
|
@ -0,0 +1,12 @@
|
|||
% !TEX root = ../Projektdokumentation.tex
|
||||
\section{Abschluss}
|
||||
|
||||
\subsection{Projektziel}
|
||||
Im Rahmen der Soll-Analyse konnten wesentliche Bestandteile des Projekts erfolgreich umgesetzt werden. Aufgrund zeitlicher Einschränkungen sowie unvorhergesehener technischer Herausforderungen war es jedoch nicht möglich, sämtliche ursprünglich geplanten Funktionen vollständig zu realisieren. Während der Entwicklungsphase wurde ein besonderer Schwerpunkt auf die technischen Grundlagen gelegt, insbesondere auf die sichere Authentifizierung, die serverseitige Logik sowie die Bereitstellung einer stabilen Infrastruktur. Monetarisierungsstrategien und benutzerorientierte Aspekte wurden in dieser Phase hingegen nur am Rande berücksichtigt.
|
||||
|
||||
Im Verlauf der Ausarbeitung wurde zudem deutlich, dass die Umsetzung einzelner Spiele deutlich komplexer ist als zunächst angenommen. Aus diesem Grund wurde die Entwicklung auf den Kern des Spielkonzepts reduziert, wohingegen begleitende Elemente wie soziale Funktionen oder ein vollständig responsives Design vorerst nicht weiter ausgearbeitet wurden. Im weiteren Projektverlauf wurde deutlich, dass die Implementierung von Poker und Plinko nicht mehr im geplanten Zeitrahmen realisiert werden konnte. Insbesondere die Entwicklung der Authentifizierungsmechanismen sowie das Deployment der Anwendung erforderten einen höheren Aufwand als erwartet.
|
||||
|
||||
\subsection{Fazit}
|
||||
Die Zusammenarbeit im Team kann als grundsätzlich positiv bewertet werden. Es sei besonders hervorgehoben, dass eine offene und funktionierende Kommunikation existierte, die es ermöglichte, Herausforderungen gemeinsam zu bewältigen und Entscheidungen transparent zu treffen. Gleichzeitig wurde jedoch Optimierungspotenzial in der Organisation und Strukturierung der Arbeitsprozesse aufgezeigt. Das Projektverhalten zeigte phasenweise Unkoordiniertheit und chaotische Züge, was die Effizienz beeinträchtigte. Das Ticket-Handling hätte klarer und konsequenter erfolgen müssen, um Aufgaben besser zu priorisieren und den Überblick über den Projektfortschritt zu wahren. Trotz der zuvor genannten strukturellen Schwächen war eine gute inhaltliche Zusammenarbeit innerhalb des Teams zu verzeichnen, in deren Folge bedeutende Meilensteine erreicht werden konnten.
|
||||
|
||||
Trotzdem war das Projekt von erheblicher Frustration geprägt, deren Hauptursache die als unzureichend empfundene Technologie, insbesondere Java, war. Gepaart mit Mängeln im Projektmanagement, wie zu spät begonnener Dokumentation und ineffektiven Retrospektiven, führte der massive Zeitdruck zu einem unbefriedigenden Ergebnis. Das Gesamterlebnis gipfelt in dem Gefühl, letztlich „garnichts“ erreicht zu haben, und dem nachdrücklichen Wunsch, Java zukünftig zu meiden
|
23
projektdokumentation/Inhalt/Auth.tex
Normal file
|
@ -0,0 +1,23 @@
|
|||
\section{Authentifizierung}
|
||||
\label{sec:Authentifizierung}
|
||||
Die Authentifizierung gegenüber der \acs{API} erfolgt über einen \acs{JWT}-Token, der dem Frontend nach Erfolgreicher Authentifizierung übergeben wird.
|
||||
Authentifizierung läuft über zwei verschiedene Wege ab:
|
||||
\subsection{Registrierung mit username/password} Der Nutzer füllt ein Registrierungs-Formular aus, welches die Anmeldedaten an die \acs{API} sendet. Diese validitert die Anmeldedaten und legt bei Erfolg einen neuen Nutzer an. Anschließend wird eine E-Mail-Verifizierungs-Mail gesendet. Bis der Link in der Verifizierungs-Mail nicht angeklickt wurde, ist der Nutzer nicht aktiv und kann sich nicht anmelden. Nach dem Klick auf den Link wird der Nutzer aktiviert und kann sich anmelden.
|
||||
|
||||
\subsection{Login mit username/password} Der Nutzer füllt ein Anmelde-Formular, welches die Anmeldedaten an die \acs{API} sendet. Diese prüft die Anmeldedaten und gibt bei Erfolg einen \acs{JWT}-Token zurück. Falls kein Nutzer mit den Anmeldedaten existiert, wird der Nutzer aufgefordert einen Account zu erstellen.
|
||||
|
||||
\begin{figure}
|
||||
\centering
|
||||
\includegraphics[width=0.3\textwidth]{login.png}
|
||||
\caption{Login-Formular der Anwendung}
|
||||
\label{fig:login}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Login über Oauth (Open Authorization)} Der Nutzer meldet sich mit einem Oauth-Provider an, in unserem Fall Google oder Github. Das Backend leitet den Nutzer zum Oauth-Provider weiter, der die Anmeldedaten prüft und bei Erfolg den Nutzer auf die Applikation weiterleitet und einen Authorization-Code zurück gibt. Mit diesem Code holt sich die \acs{API} einen \acs{JWT} vom jeweiligen Provider und holt sich Nutzer-Informationen. Mit diesen wird dann ein existierender Nutzer eingeloggt, oder registriert falls der Nutzer noch kein Konto hatte. Anschließend wird von der \acs{API} ein \acs{JWT} generiert und an das Frontend weitergegeben.
|
||||
|
||||
\begin{figure}
|
||||
\centering
|
||||
\includegraphics[width=0.45\textwidth]{oauth.png}
|
||||
\caption{OAuth-Authentifizierungsablauf}
|
||||
\label{fig:oauth}
|
||||
\end{figure}
|
52
projektdokumentation/Inhalt/Blackjack.tex
Normal file
|
@ -0,0 +1,52 @@
|
|||
\section{Blackjack}
|
||||
|
||||
\subsection{Was ist Blackjack?}
|
||||
Blackjack ist eines der populärsten Kartenspiele in Casinos weltweit und wurde als vollständiges Spiel in die Casino-Plattform integriert. Das Ziel des Spiels ist es, mit den eigenen Karten einen Wert von 21 zu erreichen oder näher an 21 heranzukommen als der Dealer, ohne dabei über 21 zu gehen (Bust). Das Spiel kombiniert Strategie und Glück, da Spieler Entscheidungen über ihre Spielzüge treffen müssen, während sie gegen einen automatisierten Dealer antreten.
|
||||
|
||||
Die Implementierung folgt den klassischen Blackjack-Regeln: Zahlenkarten haben ihren Nennwert, Bildkarten (König, Dame, Bube) zählen 10 Punkte, und Asse können je nach Situation als 1 oder 11 gewertet werden. Ein "Blackjack" (21 mit den ersten beiden Karten) zahlt mit einem Bonus von 1,5x des Einsatzes aus, während reguläre Gewinne den doppelten Einsatz zurückgeben.
|
||||
|
||||
\subsubsection{Kartensystem und Deck-Verwaltung}
|
||||
Das Blackjack-System verwendet ein vollständiges 52-Karten-Deck mit vier Farben (Herz, Karo, Kreuz, Pik) und 13 Rängen (2-10, Bube, Dame, König, Ass). Die Karten werden in der Datenbank als separate Entitäten gespeichert, wobei jede Karte einem Typ zugeordnet ist: DECK (noch im Stapel), PLAYER (Spielerhand) oder DEALER (Dealerhand).
|
||||
|
||||
Die DeckService-Klasse verwaltet die Kartenerstellung und -verteilung. Bei Spielbeginn wird ein neues, gemischtes Deck erstellt und die Karten werden entsprechend den Spielregeln verteilt: Der Spieler erhält zwei Karten, der Dealer erhält eine Karte. Die Zufallsmischung erfolgt über java.util.Random, um eine faire Kartenverteilung zu gewährleisten.
|
||||
|
||||
\subsubsection{Spielzustände und Ablauf}
|
||||
Das Spiel durchläuft verschiedene definierte Zustände:
|
||||
\begin{itemize}
|
||||
\item \textbf{IN\_PROGRESS:} Das Spiel läuft aktiv, der Spieler kann Aktionen ausführen
|
||||
\item \textbf{PLAYER\_BLACKJACK:} Der Spieler hat einen natürlichen Blackjack (21 mit zwei Karten)
|
||||
\item \textbf{PLAYER\_WON:} Der Spieler hat gewonnen ohne Blackjack
|
||||
\item \textbf{PLAYER\_LOST:} Der Spieler hat verloren oder sich überkauft
|
||||
\item \textbf{DRAW:} Unentschieden zwischen Spieler und Dealer
|
||||
\end{itemize}
|
||||
|
||||
Der Spielablauf gestaltet sich folgendermaßen:
|
||||
\begin{enumerate}
|
||||
\item Spieler startet das Spiel mit einem Einsatz
|
||||
\item Austeilung der Anfangskarten (2 für Spieler, 1 für Dealer)
|
||||
\item Spieler trifft Entscheidungen: Hit (weitere Karte), Stand (keine weitere Karte), Double Down (Einsatz verdoppeln und eine Karte)
|
||||
\item Bei Stand aktiviert sich die Dealer-Logik: Dealer zieht Karten bis mindestens 17 erreicht sind
|
||||
\item Gewinnermittlung und Auszahlung entsprechend des Ergebnisses
|
||||
\end{enumerate}
|
||||
|
||||
\subsubsection{Handwertberechnung}
|
||||
Die Berechnung des Handwerts stellt den Kern der Spiellogik dar. Zunächst werden die Grundwerte aller Karten addiert, wobei Asse initial mit 11 bewertet werden. Falls die Summe 21 übersteigt und Asse vorhanden sind, werden diese nacheinander von 11 auf 1 reduziert, bis entweder die Summe unter 22 liegt oder keine Asse mehr als 11 gewertet werden können.
|
||||
|
||||
Diese flexible Ass-Bewertung ermöglicht optimale Strategien: Ein Ass kann als "Soft" (11) oder "Hard" (1) gewertet werden, abhängig von den anderen Karten in der Hand. Die Implementierung sorgt automatisch für die bestmögliche Bewertung der Spielerhand.
|
||||
|
||||
\subsubsection{Wettsystem und Auszahlungen}
|
||||
Das Blackjack-Spiel ist vollständig in das Wettsystem der Plattform integriert. Der Einsatz wird zu Spielbeginn vom Spielerkonto abgebucht und bei Gewinn entsprechend der Auszahlungsregeln gutgeschrieben:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Blackjack:} 1,5x Einsatz plus ursprünglicher Einsatz zurück
|
||||
\item \textbf{Regulärer Gewinn:} 2x Einsatz (Verdopplung)
|
||||
\item \textbf{Unentschieden:} Ursprünglicher Einsatz wird zurückerstattet
|
||||
\item \textbf{Niederlage:} Kein Gewinn, Einsatz verloren
|
||||
\end{itemize}
|
||||
|
||||
Die Double-Down-Funktion verdoppelt den ursprünglichen Einsatz und gibt dem Spieler genau eine weitere Karte, danach ist das Spiel automatisch beendet. Diese Aktion ist nur mit den ersten beiden Karten möglich und erfordert ausreichendes Guthaben für den zusätzlichen Einsatz.
|
||||
|
||||
\subsubsection{Frontend-Integration}
|
||||
Das Frontend bietet eine vollständig animierte Blackjack-Erfahrung mit visueller Kartenrepräsentation und intuitiver Benutzeroberfläche. Die Spielerkarten werden offen dargestellt, während die zweite Dealer-Karte bis zum Spielende verdeckt bleibt. Sound-Effekte und Animationen verstärken die Spielerfahrung.
|
||||
|
||||
Die Zustandssynchronisation zwischen Frontend und Backend erfolgt über reactive Programmierung mit Angular Signals, wodurch Änderungen in Echtzeit dargestellt werden. Fehlerbehandlung sorgt für robuste Spielzustände auch bei Netzwerkproblemen, und die Benutzeroberfläche passt sich dynamisch an verschiedene Spielsituationen an.
|
53
projektdokumentation/Inhalt/CI.tex
Normal file
|
@ -0,0 +1,53 @@
|
|||
% !TEX root = ../Projektdokumentation.tex
|
||||
\section{Continuous Integration}
|
||||
\label{sec:CI}
|
||||
|
||||
Das Projekt verwendet Gitea Actions\footnote{Gitea Actions - \url{https://docs.gitea.com/usage/actions/overview}} als \ac{CI/CD}-Pipeline, welche vollständig kompatibel mit GitHub Actions ist.
|
||||
Entsprechend den Qualitätsanforderungen soll eine hohe Code-Qualität durch automatisierte Tests gewährleistet werden.
|
||||
|
||||
\subsection{Aufbau der CI-Pipeline}
|
||||
\label{sec:ci-pipeline}
|
||||
|
||||
Die Haupt-\ac{CI}-Pipeline wird durch die Datei \Datei{ci.yml} definiert und bei Pull Requests ausgelöst. Aufgrund der separaten Frontend- und Backend-Komponenten
|
||||
wurde eine \Fachbegriff{Change Detection} implementiert, welche nur relevante Tests für geänderte Bereiche ausführt.
|
||||
|
||||
Ein initialer Job identifiziert geänderte Dateien und ermöglicht eine selektive Ausführung:
|
||||
\begin{itemize}
|
||||
\item Backend-Änderungen: \Datei{backend/**}
|
||||
\item Frontend-Änderungen: \Datei{frontend/**}
|
||||
\item Workflow-Änderungen: \Datei{.gitea/workflows/**}
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{Backend-Qualitätssicherung}
|
||||
Für Backend-Änderungen werden folgende Prüfungen durchgeführt:
|
||||
\begin{itemize}
|
||||
\item \textbf{Unit Tests:} Ausführung mit \Eingabe{./gradlew test} in OpenJDK 23 Container
|
||||
\item \textbf{Checkstyle:} Code-Style-Validierung mit Caching-Mechanismus
|
||||
\item \textbf{Docker Build:} Überprüfung der Build-Funktionalität
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{Frontend-Qualitätssicherung}
|
||||
Für Frontend-Änderungen wird eine umfassende Testsuite ausgeführt:
|
||||
\begin{itemize}
|
||||
\item \textbf{ESLint:} Code-Qualitätsprüfung mit \Eingabe{bun run lint}
|
||||
\item \textbf{Prettier:} Code-Formatierungsvalidierung
|
||||
\item \textbf{Build-Test:} Produktions-Build-Validierung mit \Eingabe{bun run build}
|
||||
\item \textbf{Playwright \ac{E2E} Tests:} End-to-End-Tests mit automatischem Backend-Start
|
||||
\item \textbf{Docker Build:} Validierung der Container-Erstellung
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Release-Management}
|
||||
\label{sec:release-pipeline}
|
||||
|
||||
Das Release-Management erfolgt automatisiert durch die \Datei{release.yml} Pipeline bei Pushes auf den \texttt{main}-Branch.
|
||||
Die Implementierung folgt \Fachbegriff{Semantic Versioning}\footnote{Semantic Versioning - \url{https://semver.org/}} und \Fachbegriff{Conventional Commits}\footnote{Conventional Commits - \url{https://www.conventionalcommits.org/}}.
|
||||
|
||||
Die Release-Pipeline umfasst:
|
||||
\begin{enumerate}
|
||||
\item \textbf{Semantic Release:} Automatische Versionierung basierend auf Commit-Nachrichten
|
||||
\item \textbf{Docker Image Build:} Parallele Erstellung von Backend- und Frontend-Images
|
||||
\item \textbf{Registry Push:} Upload zur privaten Gitea Docker Registry
|
||||
\end{enumerate}
|
||||
|
||||
Die \ac{CI/CD}-Pipeline implementiert Performance-Optimierungen wie intelligentes Caching, Concurrency Control und selektive Job-Ausführung.
|
||||
Diese Automatisierung gewährleistet eine hohe Software-Qualität bei effizienten Entwicklungsprozessen.
|
41
projektdokumentation/Inhalt/Coinflip.tex
Normal file
|
@ -0,0 +1,41 @@
|
|||
\section{Coinflip}
|
||||
|
||||
\subsection{Was ist Coinflip?}
|
||||
Das Münzwurf-Spiel 'Coinflip' ist ein klassisches Glücksspiel, das in seiner digitalen Umsetzung den traditionellen Münzwurf simuliert. Das Spiel basiert auf dem einfachen Prinzip einer Münze mit zwei Seiten: Kopf und Zahl. Spieler setzen auf eine der beiden Seiten und haben eine 50\%-ige Gewinnchance. Die Einfachheit des Spiels macht es zu einem idealen Einstiegsspiel für neue Nutzer der Casino-Plattform.
|
||||
|
||||
Im Gegensatz zu komplexeren Spielen wie Dice bietet Coinflip eine feste Gewinnwahrscheinlichkeit von 50\% und einen konstanten Multiplikator von 2x. Dies bedeutet, dass Spieler bei einem Gewinn ihren Einsatz verdoppeln, während sie bei einer Niederlage ihren gesamten Einsatz verlieren.
|
||||
|
||||
\subsubsection{Zufallszahlengenerierung}
|
||||
Die Implementierung verwendet die Standardklasse java.util.Random zur Generierung des Münzwurfs.
|
||||
Die Zufallsgenerierung erzeugt einen booleschen Wert, der anschließend einer der beiden Münzseiten zugeordnet wird.
|
||||
Diese binäre Entscheidung gewährleistet die faire 50:50-Verteilung, die für ein authentisches Münzwurf-Erlebnis erforderlich ist.
|
||||
|
||||
\subsubsection{Spielablauf und Datenfluss}
|
||||
|
||||
Der Spielablauf von Coinflip folgt einem strukturierten Datenfluss zwischen Frontend und Backend. Der Controller empfängt die Spielanfrage mit folgenden Parametern:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Einsatz:} Der gesetzte Münzbetrag.
|
||||
\item \textbf{Gewählte Seite:} Die vom Spieler gewählte Münzseite (Kopf oder Zahl).
|
||||
\end{itemize}
|
||||
|
||||
Nach dem Erhalt der Anfrage führt der Controller eine Guthabenprüfung durch. Bei ausreichendem Guthaben wird die Anfrage an die Service-Schicht weitergeleitet, andernfalls wird eine entsprechende Fehlermeldung zurückgegeben.
|
||||
|
||||
Die Service-Klasse verarbeitet die Spiellogik in folgender Reihenfolge:
|
||||
\begin{enumerate}
|
||||
\item Abbuchung des Einsatzes vom Spielerkonto.
|
||||
\item Generierung des zufälligen Münzwurfs (Kopf oder Zahl).
|
||||
\item Vergleich zwischen gewählter Seite und Wurfergebnis.
|
||||
\item Bei einem Gewinn: Gutschrift des doppelten Einsatzes auf das Spielerkonto.
|
||||
\item Rückgabe des Spielergebnisses an das Frontend.
|
||||
\end{enumerate}
|
||||
|
||||
Das Spielergebnis wird strukturiert an das Frontend übermittelt und enthält:
|
||||
\begin{itemize}
|
||||
\item Gewinnstatus (gewonnen/verloren)
|
||||
\item Auszahlungsbetrag (bei Gewinn: 2x Einsatz)
|
||||
\item Geworfene Münzseite
|
||||
\end{itemize}
|
||||
|
||||
\subsubsection{Implementierungsdetails}
|
||||
Die vollständige Implementierung der Coinflip-Funktionalität umfasst verschiedene Architekturschichten: Das Angular Frontend-Component (siehe \ref{app:FrontendComponent}), den Spring Boot REST Controller (siehe \ref{app:ControllerSchicht}) und die Service-Schicht mit der Geschäftslogik (siehe \ref{app:ServiceSchicht}). Zusätzlich wird die Benutzer-Entity (siehe \ref{app:PersistierungSchicht}) für die Guthaben-Verwaltung verwendet.
|