Compare commits
28 Commits
b34efb8c1c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| af971b7b83 | |||
| 4a087a4f24 | |||
| 502d48da99 | |||
| cd2994d7eb | |||
| dc9a60a7a2 | |||
| 44e3ad94e0 | |||
| 296c7fefc5 | |||
| 9cbbd2d4f2 | |||
| c4febd7036 | |||
| 324ce141d0 | |||
| e5eacd8725 | |||
| 33b21959d8 | |||
| a8d827eace | |||
| d0785e04e1 | |||
| a4d2e571d5 | |||
| 008ca7f65d | |||
| 587467321d | |||
| ed5d14989a | |||
| a8b7cc2ffd | |||
| 21c07adf54 | |||
| d2780e9295 | |||
| 62dc7bd93f | |||
| 3aa191cd3d | |||
| a7fd925187 | |||
| ce0f5a3a20 | |||
| a160fb835a | |||
| 7ed6f13a30 | |||
| add81d6d4a |
34
.gitea/workflows/deploy.yml
Normal file
34
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build -t silverlabs-website:latest -t silverlabs-website:${{ github.sha }} .
|
||||||
|
- name: Deploy to server
|
||||||
|
run: |
|
||||||
|
# Install SSH tools
|
||||||
|
apt-get update && apt-get install -y sshpass
|
||||||
|
# Save and transfer image
|
||||||
|
docker save silverlabs-website:latest | gzip > /tmp/silverlabs-website.tar.gz
|
||||||
|
sshpass -p "${{ secrets.DEPLOY_PASSWORD }}" scp -o StrictHostKeyChecking=no /tmp/silverlabs-website.tar.gz sysadmin@10.0.0.247:/tmp/
|
||||||
|
# SSH and deploy
|
||||||
|
sshpass -p "${{ secrets.DEPLOY_PASSWORD }}" ssh -o StrictHostKeyChecking=no sysadmin@10.0.0.247 bash -s << 'EOF'
|
||||||
|
set -e
|
||||||
|
docker load < /tmp/silverlabs-website.tar.gz
|
||||||
|
docker stop silverlabs-website 2>/dev/null || true
|
||||||
|
docker rm silverlabs-website 2>/dev/null || true
|
||||||
|
docker run -d \
|
||||||
|
--name silverlabs-website \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 8100:80 \
|
||||||
|
silverlabs-website:latest
|
||||||
|
docker ps | grep silverlabs-website
|
||||||
|
rm /tmp/silverlabs-website.tar.gz
|
||||||
|
echo "Deployment complete!"
|
||||||
|
EOF
|
||||||
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# .NET Build artifacts
|
||||||
|
BlazorApp/bin/
|
||||||
|
BlazorApp/obj/
|
||||||
|
BlazorApp/.vs/
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Rr]elease/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
|
||||||
|
# Visual Studio cache/options directory
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
# Rider
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Backup folder
|
||||||
|
old-static-site/
|
||||||
|
|
||||||
|
# Publish output
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# OS Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
stages:
|
|
||||||
- build
|
|
||||||
- deploy
|
|
||||||
|
|
||||||
variables:
|
|
||||||
DOCKER_IMAGE: silverlabs-website
|
|
||||||
|
|
||||||
# Use Docker image
|
|
||||||
image: docker:latest
|
|
||||||
|
|
||||||
# Build Docker image
|
|
||||||
build:website:
|
|
||||||
stage: build
|
|
||||||
services:
|
|
||||||
- docker:dind
|
|
||||||
script:
|
|
||||||
- echo "Building SilverLabs Website Docker image..."
|
|
||||||
- docker build -t $DOCKER_IMAGE:latest -t $DOCKER_IMAGE:${CI_COMMIT_SHORT_SHA} .
|
|
||||||
- docker images
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- main
|
|
||||||
|
|
||||||
# Deploy to production server (PORTAINER-01)
|
|
||||||
deploy:production:
|
|
||||||
stage: deploy
|
|
||||||
image: docker:latest
|
|
||||||
services:
|
|
||||||
- docker:dind
|
|
||||||
dependencies:
|
|
||||||
- build:website
|
|
||||||
before_script:
|
|
||||||
- apk add --no-cache sshpass openssh-client
|
|
||||||
script:
|
|
||||||
- echo "🚀 Deploying SilverLabs Website to silverlabs.uk (PORTAINER-01)"
|
|
||||||
|
|
||||||
# Build Docker image
|
|
||||||
- docker build -t $DOCKER_IMAGE:latest -t $DOCKER_IMAGE:${CI_COMMIT_SHORT_SHA} .
|
|
||||||
|
|
||||||
# Save and transfer image to server
|
|
||||||
- docker save $DOCKER_IMAGE:latest | gzip > /tmp/$DOCKER_IMAGE-image.tar.gz
|
|
||||||
- sshpass -p "Phenom12#." scp -o StrictHostKeyChecking=no /tmp/$DOCKER_IMAGE-image.tar.gz sysadmin@10.0.0.51:/tmp/
|
|
||||||
|
|
||||||
# Deploy on server
|
|
||||||
- |
|
|
||||||
sshpass -p "Phenom12#." ssh -o StrictHostKeyChecking=no sysadmin@10.0.0.51 << 'ENDSSH'
|
|
||||||
cd /opt/silverlabs/website
|
|
||||||
docker load < /tmp/$DOCKER_IMAGE-image.tar.gz
|
|
||||||
|
|
||||||
# Stop and remove old container
|
|
||||||
docker stop silverlabs-website 2>/dev/null || true
|
|
||||||
docker rm silverlabs-website 2>/dev/null || true
|
|
||||||
|
|
||||||
# Start new container
|
|
||||||
docker run -d \
|
|
||||||
--name silverlabs-website \
|
|
||||||
--restart unless-stopped \
|
|
||||||
-p 8100:80 \
|
|
||||||
silverlabs-website:latest
|
|
||||||
|
|
||||||
docker ps | grep silverlabs-website
|
|
||||||
rm /tmp/silverlabs-website-image.tar.gz
|
|
||||||
echo "✅ Deployment complete!"
|
|
||||||
ENDSSH
|
|
||||||
environment:
|
|
||||||
name: production
|
|
||||||
url: https://silverlabs.uk
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- main
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
21
BlazorApp/Components/App.razor
Normal file
21
BlazorApp/Components/App.razor
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<base href="/" />
|
||||||
|
<link rel="stylesheet" href="@Assets["styles.css"]" />
|
||||||
|
<link rel="stylesheet" href="@Assets["sdk-styles.css"]" />
|
||||||
|
<link rel="stylesheet" href="@Assets["developers-styles.css"]" />
|
||||||
|
<ImportMap />
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
<HeadOutlet />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<Routes />
|
||||||
|
<script src="_framework/blazor.web.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
9
BlazorApp/Components/Layout/MainLayout.razor
Normal file
9
BlazorApp/Components/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
@Body
|
||||||
|
|
||||||
|
<div id="blazor-error-ui" data-nosnippet>
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href="." class="reload">Reload</a>
|
||||||
|
<span class="dismiss">🗙</span>
|
||||||
|
</div>
|
||||||
98
BlazorApp/Components/Layout/MainLayout.razor.css
Normal file
98
BlazorApp/Components/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #d6d5d5;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:first-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640.98px) {
|
||||||
|
.top-row {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.page {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
height: 100vh;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth ::deep a:first-child {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row, article {
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
padding-right: 1.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui {
|
||||||
|
color-scheme: light only;
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
30
BlazorApp/Components/Layout/NavMenu.razor
Normal file
30
BlazorApp/Components/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="">SilverLabs.Website</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||||
|
|
||||||
|
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||||
|
<nav class="nav flex-column">
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||||
|
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="counter">
|
||||||
|
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="weather">
|
||||||
|
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
105
BlazorApp/Components/Layout/NavMenu.razor.css
Normal file
105
BlazorApp/Components/Layout/NavMenu.razor.css
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
.navbar-toggler {
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler:checked {
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
min-height: 3.5rem;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
top: -1px;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-house-door-fill-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-plus-square-fill-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-list-nested-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:first-of-type {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:last-of-type {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep .nav-link {
|
||||||
|
color: #d7d7d7;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 3rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep a.active {
|
||||||
|
background-color: rgba(255,255,255,0.37);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scrollable {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler:checked ~ .nav-scrollable {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.navbar-toggler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scrollable {
|
||||||
|
/* Never collapse the sidebar for wide screens */
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
/* Allow sidebar to scroll for tall menus */
|
||||||
|
height: calc(100vh - 3.5rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
BlazorApp/Components/Pages/Counter.razor
Normal file
19
BlazorApp/Components/Pages/Counter.razor
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@page "/counter"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
<PageTitle>Counter</PageTitle>
|
||||||
|
|
||||||
|
<h1>Counter</h1>
|
||||||
|
|
||||||
|
<p role="status">Current count: @currentCount</p>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private int currentCount = 0;
|
||||||
|
|
||||||
|
private void IncrementCount()
|
||||||
|
{
|
||||||
|
currentCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
357
BlazorApp/Components/Pages/DeploymentConfirm.razor
Normal file
357
BlazorApp/Components/Pages/DeploymentConfirm.razor
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
@page "/developers/confirm/{Token}"
|
||||||
|
@inject HttpClient Http
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject IConfiguration Configuration
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
<PageTitle>Activate Your Accounts - SilverLabs</PageTitle>
|
||||||
|
|
||||||
|
<div class="main-content visible">
|
||||||
|
<header class="header">
|
||||||
|
<img src="logo.png" alt="SilverLabs Logo" class="logo">
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dev-container">
|
||||||
|
<div class="dev-header">
|
||||||
|
<h1>Activate Your Accounts</h1>
|
||||||
|
<p class="dev-subtitle">Confirm your identity to provision your SilverLabs developer accounts.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_loading)
|
||||||
|
{
|
||||||
|
<div class="dev-section" style="text-align: center; padding: 3rem;">
|
||||||
|
<div class="btn-spinner" style="width: 32px; height: 32px; margin: 0 auto 1rem;"></div>
|
||||||
|
<p style="color: rgba(255,255,255,0.6);">Loading deployment details...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_invalidToken)
|
||||||
|
{
|
||||||
|
<div class="dev-section" style="text-align: center; padding: 3rem;">
|
||||||
|
<div class="confirm-icon confirm-icon-error">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||||
|
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 style="color: #f87171; margin-bottom: 0.75rem;">Invalid or Expired Link</h2>
|
||||||
|
<p style="color: rgba(255,255,255,0.6); max-width: 400px; margin: 0 auto;">This confirmation link is no longer valid. It may have expired or already been used. Please contact an administrator if you need a new link.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_provisioned)
|
||||||
|
{
|
||||||
|
<div class="dev-success-panel">
|
||||||
|
<div class="success-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2>Accounts Activated</h2>
|
||||||
|
<p>@_resultMessage</p>
|
||||||
|
<div class="confirm-services">
|
||||||
|
<a href="https://mail.silverlined.uk" target="_blank" class="confirm-service-item confirm-service-link">
|
||||||
|
<strong>Email</strong>
|
||||||
|
<span>@(_username)@@silverlabs.uk</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://ops.silverlined.uk" target="_blank" class="confirm-service-item confirm-service-link">
|
||||||
|
<strong>Mattermost</strong>
|
||||||
|
<span>Team chat & collaboration</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://git.silverlabs.uk" target="_blank" class="confirm-service-item confirm-service-link">
|
||||||
|
<strong>Gitea</strong>
|
||||||
|
<span>Source code repositories</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://silverdesk.silverlabs.uk" target="_blank" class="confirm-service-item confirm-service-link">
|
||||||
|
<strong>SilverDESK</strong>
|
||||||
|
<span>Support & tickets</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="confirm-email-config">
|
||||||
|
<strong>Email Client Setup</strong>
|
||||||
|
<div class="confirm-email-detail"><span>IMAP:</span> mail.silverlined.uk:993 (SSL)</div>
|
||||||
|
<div class="confirm-email-detail"><span>SMTP:</span> mail.silverlined.uk:465 (SSL)</div>
|
||||||
|
</div>
|
||||||
|
<p class="dev-account-note">All accounts use the same password you just entered.</p>
|
||||||
|
<div class="dev-success-actions">
|
||||||
|
<a href="https://silverdesk.silverlabs.uk" target="_blank" class="dev-btn dev-btn-primary">Go to SilverDESK</a>
|
||||||
|
<a href="/" class="dev-btn dev-btn-secondary">Back to Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="dev-section">
|
||||||
|
<h2 class="dev-section-title">Confirm Your Identity</h2>
|
||||||
|
<p class="dev-section-desc">Enter your SilverDESK password to activate your accounts. All services will use this same password.</p>
|
||||||
|
|
||||||
|
<div class="confirm-user-info">
|
||||||
|
<div class="confirm-user-field">
|
||||||
|
<span class="confirm-label">Username</span>
|
||||||
|
<span class="confirm-value">@_username</span>
|
||||||
|
</div>
|
||||||
|
<div class="confirm-user-field">
|
||||||
|
<span class="confirm-label">Email</span>
|
||||||
|
<span class="confirm-value">@_email</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-top: 1.5rem; max-width: 400px;">
|
||||||
|
<label for="password">SilverDESK Password</label>
|
||||||
|
<input id="password" type="password" class="form-input" @bind="_password"
|
||||||
|
@bind:event="oninput" @onkeydown="HandleKeyDown" placeholder="Enter your password" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||||
|
{
|
||||||
|
<div class="dev-error" style="margin-top: 1rem;">@_errorMessage</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div style="margin-top: 1.5rem;">
|
||||||
|
<button class="dev-btn dev-btn-primary" disabled="@_submitting" @onclick="HandleConfirm">
|
||||||
|
@if (_submitting)
|
||||||
|
{
|
||||||
|
<span class="btn-spinner"></span>
|
||||||
|
<span>Activating accounts...</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Activate My Accounts</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<a href="/" class="back-link">← Back to SilverLabs Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.confirm-icon-error {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
background: rgba(248, 113, 113, 0.15);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-icon-error svg {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
stroke: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-user-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-user-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-label {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-value {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #4DD0E1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-services {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-service-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-service-link {
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid rgba(77, 208, 225, 0.15);
|
||||||
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-service-link:hover {
|
||||||
|
background: rgba(77, 208, 225, 0.08);
|
||||||
|
border-color: rgba(77, 208, 225, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-service-item strong {
|
||||||
|
color: #4DD0E1;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-service-item span {
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-email-config {
|
||||||
|
margin: 1rem auto;
|
||||||
|
max-width: 360px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-email-config strong {
|
||||||
|
display: block;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-email-detail {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
padding: 0.15rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-email-detail span {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media (max-width: 768px) {
|
||||||
|
.confirm-user-info {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-services {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string Token { get; set; } = "";
|
||||||
|
|
||||||
|
private bool _loading = true;
|
||||||
|
private bool _invalidToken;
|
||||||
|
private bool _provisioned;
|
||||||
|
private bool _submitting;
|
||||||
|
private string? _username;
|
||||||
|
private string? _email;
|
||||||
|
private string? _password;
|
||||||
|
private string? _errorMessage;
|
||||||
|
private string? _resultMessage;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!firstRender) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var baseUrl = Navigation.BaseUri.TrimEnd('/');
|
||||||
|
using var client = new HttpClient();
|
||||||
|
var response = await client.GetAsync($"{baseUrl}/api/developers/deployment-info/{Token}");
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var data = await response.Content.ReadFromJsonAsync<DeploymentInfo>();
|
||||||
|
_username = data?.Username;
|
||||||
|
_email = data?.Email;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_invalidToken = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_invalidToken = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == "Enter" && !_submitting && !string.IsNullOrEmpty(_password))
|
||||||
|
await HandleConfirm();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleConfirm()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_password))
|
||||||
|
{
|
||||||
|
_errorMessage = "Please enter your password.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_errorMessage = null;
|
||||||
|
_submitting = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var baseUrl = Navigation.BaseUri.TrimEnd('/');
|
||||||
|
using var client = new HttpClient();
|
||||||
|
var payload = new { token = Token, password = _password };
|
||||||
|
var response = await client.PostAsJsonAsync($"{baseUrl}/api/developers/confirm-deployment", payload);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ProvisionResult>();
|
||||||
|
_resultMessage = result?.Message ?? "Accounts activated successfully.";
|
||||||
|
_provisioned = true;
|
||||||
|
}
|
||||||
|
else if ((int)response.StatusCode == 401)
|
||||||
|
{
|
||||||
|
_errorMessage = "Incorrect password. Please enter the password you created when you applied.";
|
||||||
|
}
|
||||||
|
else if ((int)response.StatusCode == 404)
|
||||||
|
{
|
||||||
|
_invalidToken = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_errorMessage = "Something went wrong. Please try again or contact an administrator.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_errorMessage = "Connection error. Please try again.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_submitting = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DeploymentInfo(string Username, string Email, string FullName, DateTime ExpiresAt);
|
||||||
|
private record ProvisionResult(bool Success, string Message);
|
||||||
|
}
|
||||||
492
BlazorApp/Components/Pages/Developers.razor
Normal file
492
BlazorApp/Components/Pages/Developers.razor
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
@page "/developers"
|
||||||
|
@using SilverLabs.Website.Models
|
||||||
|
@using SilverLabs.Website.Services
|
||||||
|
@inject DeveloperApplicationService ApplicationService
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
<PageTitle>Join the Team - SilverLabs</PageTitle>
|
||||||
|
|
||||||
|
<div class="main-content visible">
|
||||||
|
<header class="header">
|
||||||
|
<img src="logo.png" alt="SilverLabs Logo" class="logo">
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dev-container">
|
||||||
|
<div class="dev-header">
|
||||||
|
<h1>Join the SilverLabs Team</h1>
|
||||||
|
<p class="dev-subtitle">Help us build privacy-first infrastructure. Whether you test our products or write code, there's a place for you.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_submitted)
|
||||||
|
{
|
||||||
|
<div class="dev-success-panel">
|
||||||
|
<div class="success-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2>Application Submitted</h2>
|
||||||
|
<p>@_resultMessage</p>
|
||||||
|
<p class="dev-account-note">Your SilverDESK account has been created. Log in with the password you just chose to track your application.</p>
|
||||||
|
<div class="dev-success-actions">
|
||||||
|
<a href="https://silverdesk.silverlabs.uk/login?username=@(Uri.EscapeDataString(_application.DesiredUsername ?? ""))" target="_blank" class="dev-btn dev-btn-primary">Log in to SilverDESK</a>
|
||||||
|
<a href="/" class="dev-btn dev-btn-secondary">Back to Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<EditForm Model="_application" OnValidSubmit="HandleSubmit" FormName="developer-application">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
|
||||||
|
<!-- Role Selector -->
|
||||||
|
<div class="dev-section">
|
||||||
|
<h2 class="dev-section-title">Choose Your Role</h2>
|
||||||
|
<div class="role-selector">
|
||||||
|
<div class="role-card @(_application.Role == ApplicationRole.Tester ? "role-active" : "")"
|
||||||
|
@onclick="() => SelectRole(ApplicationRole.Tester)">
|
||||||
|
<div class="role-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||||
|
<polyline points="14 2 14 8 20 8"></polyline>
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||||
|
<polyline points="10 9 9 9 8 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>Product Tester</h3>
|
||||||
|
<p>Test our apps across devices, find bugs, and provide feedback that shapes our products.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="role-card @(_application.Role == ApplicationRole.Developer ? "role-active" : "")"
|
||||||
|
@onclick="() => SelectRole(ApplicationRole.Developer)">
|
||||||
|
<div class="role-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="16 18 22 12 16 6"></polyline>
|
||||||
|
<polyline points="8 6 2 12 8 18"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>Developer</h3>
|
||||||
|
<p>Contribute code, build modules, and help architect privacy-first solutions.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ValidationMessage For="() => _application.Role" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Personal Details -->
|
||||||
|
<div class="dev-section">
|
||||||
|
<h2 class="dev-section-title">About You</h2>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fullName">Full Name</label>
|
||||||
|
<InputText id="fullName" @bind-Value="_application.FullName" class="form-input" placeholder="Jane Doe" />
|
||||||
|
<ValidationMessage For="() => _application.FullName" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email Address (optional)</label>
|
||||||
|
<InputText id="email" @bind-Value="_application.Email" class="form-input" placeholder="jane@example.com" />
|
||||||
|
<span class="form-hint">Leave blank to use your @@silverlabs.uk address</span>
|
||||||
|
<ValidationMessage For="() => _application.Email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Desired Username</label>
|
||||||
|
<input id="username" class="form-input" placeholder="janedoe" value="@_application.DesiredUsername"
|
||||||
|
@oninput="OnUsernameInput" @onfocusout="OnUsernameBlur" autocomplete="off" />
|
||||||
|
<span class="form-hint">3–30 characters: letters, numbers, hyphens and underscores</span>
|
||||||
|
@if (!string.IsNullOrEmpty(_usernameFormatError))
|
||||||
|
{
|
||||||
|
<span class="username-status username-format-error">@_usernameFormatError</span>
|
||||||
|
}
|
||||||
|
else if (_usernameCheckState == UsernameCheckState.Checking)
|
||||||
|
{
|
||||||
|
<span class="username-status username-checking">Checking availability...</span>
|
||||||
|
}
|
||||||
|
else if (_usernameCheckState == UsernameCheckState.Available)
|
||||||
|
{
|
||||||
|
<span class="username-status username-available">✓ Username is available</span>
|
||||||
|
}
|
||||||
|
else if (_usernameCheckState == UsernameCheckState.Taken)
|
||||||
|
{
|
||||||
|
<span class="username-status username-taken">✗ Username is already taken</span>
|
||||||
|
}
|
||||||
|
else if (_usernameCheckState == UsernameCheckState.Error)
|
||||||
|
{
|
||||||
|
<span class="username-status username-error">⚠ Could not check availability — you can still submit</span>
|
||||||
|
}
|
||||||
|
<ValidationMessage For="() => _application.DesiredUsername" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="timezone">Timezone</label>
|
||||||
|
<InputSelect id="timezone" @bind-Value="_application.Timezone" class="form-input">
|
||||||
|
<option value="">Select your timezone...</option>
|
||||||
|
@foreach (var tz in _timezones)
|
||||||
|
{
|
||||||
|
<option value="@tz.Id">@tz.Label</option>
|
||||||
|
}
|
||||||
|
</InputSelect>
|
||||||
|
<ValidationMessage For="() => _application.Timezone" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div class="dev-section">
|
||||||
|
<h2 class="dev-section-title">Create Your Password</h2>
|
||||||
|
<p class="dev-section-desc">This will be your password for SilverDESK and associated services.</p>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<InputText id="password" type="password" @bind-Value="_application.Password" class="form-input" placeholder="Min. 8 characters" />
|
||||||
|
<span class="form-hint">Must include uppercase, lowercase, and a number</span>
|
||||||
|
<ValidationMessage For="() => _application.Password" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirmPassword">Confirm Password</label>
|
||||||
|
<InputText id="confirmPassword" type="password" @bind-Value="_application.ConfirmPassword" class="form-input" placeholder="Re-enter your password" />
|
||||||
|
<ValidationMessage For="() => _application.ConfirmPassword" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Platforms -->
|
||||||
|
<div class="dev-section">
|
||||||
|
<h2 class="dev-section-title">Devices & Platforms</h2>
|
||||||
|
<p class="dev-section-desc">Which platforms do you use or have access to?</p>
|
||||||
|
<div class="platform-grid">
|
||||||
|
@foreach (var platform in _availablePlatforms)
|
||||||
|
{
|
||||||
|
var isChecked = _application.Platforms.Contains(platform);
|
||||||
|
<label class="platform-chip @(isChecked ? "platform-active" : "")">
|
||||||
|
<input type="checkbox" checked="@isChecked"
|
||||||
|
@onchange="() => TogglePlatform(platform)" />
|
||||||
|
<span>@platform</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<ValidationMessage For="() => _application.Platforms" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Role-Specific Assessment -->
|
||||||
|
@if (_application.Role == ApplicationRole.Tester)
|
||||||
|
{
|
||||||
|
<div class="dev-section">
|
||||||
|
<h2 class="dev-section-title">About Your Experience</h2>
|
||||||
|
<p class="dev-section-desc">Help us understand your background — there are no wrong answers.</p>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||||
|
<label>How well do you understand the internet and online services?</label>
|
||||||
|
<div class="star-rating">
|
||||||
|
@for (int i = 1; i <= 5; i++)
|
||||||
|
{
|
||||||
|
var rating = i;
|
||||||
|
<span class="star-rating-star @(rating <= (_internetHover > 0 ? _internetHover : (_application.InternetUnderstanding ?? 0)) ? "star-filled" : "")"
|
||||||
|
@onclick="() => _application.InternetUnderstanding = rating"
|
||||||
|
@onmouseover="() => _internetHover = rating"
|
||||||
|
@onmouseout="() => _internetHover = 0">@(rating <= (_internetHover > 0 ? _internetHover : (_application.InternetUnderstanding ?? 0)) ? "\u2605" : "\u2606")</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="rating-labels">
|
||||||
|
<span>Beginner</span>
|
||||||
|
<span>Expert</span>
|
||||||
|
</div>
|
||||||
|
<ValidationMessage For="() => _application.InternetUnderstanding" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||||
|
<label>How much do you enjoy trying new software and finding issues?</label>
|
||||||
|
<div class="star-rating">
|
||||||
|
@for (int i = 1; i <= 5; i++)
|
||||||
|
{
|
||||||
|
var rating = i;
|
||||||
|
<span class="star-rating-star @(rating <= (_testingHover > 0 ? _testingHover : (_application.EnjoysTesting ?? 0)) ? "star-filled" : "")"
|
||||||
|
@onclick="() => _application.EnjoysTesting = rating"
|
||||||
|
@onmouseover="() => _testingHover = rating"
|
||||||
|
@onmouseout="() => _testingHover = 0">@(rating <= (_testingHover > 0 ? _testingHover : (_application.EnjoysTesting ?? 0)) ? "\u2605" : "\u2606")</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="rating-labels">
|
||||||
|
<span>Not really</span>
|
||||||
|
<span>Love it</span>
|
||||||
|
</div>
|
||||||
|
<ValidationMessage For="() => _application.EnjoysTesting" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="additionalNotes">Anything else you'd like us to know? (optional)</label>
|
||||||
|
<InputTextArea id="additionalNotes" @bind-Value="_application.AdditionalNotes" class="form-input form-textarea"
|
||||||
|
placeholder="Previous testing experience, specific interests, etc." rows="3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="dev-section">
|
||||||
|
<h2 class="dev-section-title">Your Skills</h2>
|
||||||
|
<p class="dev-section-desc">Select your experience level and the technologies you work with.</p>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||||
|
<label>Experience Level</label>
|
||||||
|
<div class="experience-selector">
|
||||||
|
@foreach (var range in SkillCatalog.ExperienceRanges)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="exp-btn @(_application.ExperienceRange == range ? "exp-active" : "")"
|
||||||
|
@onclick="() => _application.ExperienceRange = range">@range</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<ValidationMessage For="() => _application.ExperienceRange" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||||
|
<label>Technologies (select all that apply)</label>
|
||||||
|
@foreach (var category in SkillCatalog.SkillCategories)
|
||||||
|
{
|
||||||
|
<div class="skill-category-label">@category.Key</div>
|
||||||
|
<div class="skill-bubbles">
|
||||||
|
@foreach (var skill in category.Value)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="skill-bubble @(_application.SelectedSkills.Contains(skill) ? "skill-active" : "")"
|
||||||
|
@onclick="() => ToggleSkill(skill)">@skill</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<ValidationMessage For="() => _application.SelectedSkills" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="additionalNotes">Anything not listed above? (optional)</label>
|
||||||
|
<InputTextArea id="additionalNotes" @bind-Value="_application.AdditionalNotes" class="form-input form-textarea"
|
||||||
|
placeholder="Other skills, open-source contributions, areas of interest..." rows="3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- What You Get -->
|
||||||
|
<div class="dev-section dev-perks">
|
||||||
|
<h2 class="dev-section-title">What You'll Get</h2>
|
||||||
|
<div class="perks-grid">
|
||||||
|
<div class="perk-item">
|
||||||
|
<strong>@@@("username")@@silverlabs.uk</strong>
|
||||||
|
<span>Your own SilverLabs email</span>
|
||||||
|
</div>
|
||||||
|
<div class="perk-item">
|
||||||
|
<strong>SilverDESK</strong>
|
||||||
|
<span>Project management & issue tracking</span>
|
||||||
|
</div>
|
||||||
|
<div class="perk-item">
|
||||||
|
<strong>Mattermost</strong>
|
||||||
|
<span>Team chat & collaboration</span>
|
||||||
|
</div>
|
||||||
|
<div class="perk-item">
|
||||||
|
<strong>Gitea Access</strong>
|
||||||
|
<span>Source code repositories</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
<div class="dev-submit-area">
|
||||||
|
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||||
|
{
|
||||||
|
<div class="dev-error">@_errorMessage</div>
|
||||||
|
}
|
||||||
|
<button type="submit" class="dev-btn dev-btn-primary" disabled="@IsSubmitDisabled">
|
||||||
|
@if (_submitting)
|
||||||
|
{
|
||||||
|
<span class="btn-spinner"></span>
|
||||||
|
<span>Submitting...</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Submit Application</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</EditForm>
|
||||||
|
}
|
||||||
|
|
||||||
|
<a href="/" class="back-link">← Back to SilverLabs Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private DeveloperApplication _application = new() { Role = ApplicationRole.Tester };
|
||||||
|
private bool _submitting;
|
||||||
|
private bool _submitted;
|
||||||
|
private string? _resultMessage;
|
||||||
|
private string? _errorMessage;
|
||||||
|
private int _internetHover;
|
||||||
|
private int _testingHover;
|
||||||
|
|
||||||
|
private UsernameCheckState _usernameCheckState = UsernameCheckState.None;
|
||||||
|
private string? _usernameFormatError;
|
||||||
|
private string? _lastCheckedUsername;
|
||||||
|
|
||||||
|
private readonly string[] _availablePlatforms = { "Windows", "macOS", "Linux", "Android", "iOS", "Other" };
|
||||||
|
|
||||||
|
private static readonly List<(string Id, string Label)> _timezones = new()
|
||||||
|
{
|
||||||
|
("Pacific/Midway", "(UTC-11:00) Midway Island"),
|
||||||
|
("Pacific/Honolulu", "(UTC-10:00) Hawaii"),
|
||||||
|
("America/Anchorage", "(UTC-09:00) Alaska"),
|
||||||
|
("America/Los_Angeles", "(UTC-08:00) Pacific Time (US & Canada)"),
|
||||||
|
("America/Denver", "(UTC-07:00) Mountain Time (US & Canada)"),
|
||||||
|
("America/Chicago", "(UTC-06:00) Central Time (US & Canada)"),
|
||||||
|
("America/New_York", "(UTC-05:00) Eastern Time (US & Canada)"),
|
||||||
|
("America/Caracas", "(UTC-04:00) Venezuela"),
|
||||||
|
("America/Halifax", "(UTC-04:00) Atlantic Time (Canada)"),
|
||||||
|
("America/Sao_Paulo", "(UTC-03:00) Brazil"),
|
||||||
|
("Atlantic/South_Georgia","(UTC-02:00) Mid-Atlantic"),
|
||||||
|
("Atlantic/Azores", "(UTC-01:00) Azores"),
|
||||||
|
("Europe/London", "(UTC+00:00) London, Dublin, Lisbon"),
|
||||||
|
("Europe/Berlin", "(UTC+01:00) Berlin, Paris, Amsterdam"),
|
||||||
|
("Europe/Bucharest", "(UTC+02:00) Bucharest, Helsinki, Athens"),
|
||||||
|
("Europe/Moscow", "(UTC+03:00) Moscow, Istanbul"),
|
||||||
|
("Asia/Dubai", "(UTC+04:00) Dubai, Baku"),
|
||||||
|
("Asia/Karachi", "(UTC+05:00) Karachi, Tashkent"),
|
||||||
|
("Asia/Kolkata", "(UTC+05:30) Mumbai, New Delhi"),
|
||||||
|
("Asia/Dhaka", "(UTC+06:00) Dhaka, Almaty"),
|
||||||
|
("Asia/Bangkok", "(UTC+07:00) Bangkok, Jakarta"),
|
||||||
|
("Asia/Shanghai", "(UTC+08:00) Beijing, Singapore, Perth"),
|
||||||
|
("Asia/Tokyo", "(UTC+09:00) Tokyo, Seoul"),
|
||||||
|
("Australia/Sydney", "(UTC+10:00) Sydney, Melbourne"),
|
||||||
|
("Pacific/Noumea", "(UTC+11:00) Solomon Islands"),
|
||||||
|
("Pacific/Auckland", "(UTC+12:00) Auckland, Fiji"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly System.Text.RegularExpressions.Regex UsernamePattern =
|
||||||
|
new(@"^[a-zA-Z0-9_-]{3,30}$", System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private enum UsernameCheckState { None, Checking, Available, Taken, Error }
|
||||||
|
|
||||||
|
private bool IsSubmitDisabled =>
|
||||||
|
_submitting || _usernameCheckState == UsernameCheckState.Taken || _usernameCheckState == UsernameCheckState.Checking;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender && string.IsNullOrEmpty(_application.Timezone))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var detectedTz = await JS.InvokeAsync<string>("eval", "Intl.DateTimeFormat().resolvedOptions().timeZone");
|
||||||
|
if (!string.IsNullOrEmpty(detectedTz) && _timezones.Any(tz => tz.Id == detectedTz))
|
||||||
|
{
|
||||||
|
_application.Timezone = detectedTz;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Browser may not support Intl API — ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectRole(ApplicationRole role)
|
||||||
|
{
|
||||||
|
_application.Role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TogglePlatform(string platform)
|
||||||
|
{
|
||||||
|
if (_application.Platforms.Contains(platform))
|
||||||
|
_application.Platforms.Remove(platform);
|
||||||
|
else
|
||||||
|
_application.Platforms.Add(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleSkill(string skill)
|
||||||
|
{
|
||||||
|
if (_application.SelectedSkills.Contains(skill))
|
||||||
|
_application.SelectedSkills.Remove(skill);
|
||||||
|
else
|
||||||
|
_application.SelectedSkills.Add(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUsernameInput(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
var username = e.Value?.ToString() ?? "";
|
||||||
|
_application.DesiredUsername = username;
|
||||||
|
|
||||||
|
// Reset API check state while typing — we'll check on blur
|
||||||
|
_usernameCheckState = UsernameCheckState.None;
|
||||||
|
_usernameFormatError = null;
|
||||||
|
|
||||||
|
// Show inline format feedback as they type
|
||||||
|
if (username.Length > 0 && username.Length < 3)
|
||||||
|
{
|
||||||
|
_usernameFormatError = "Username must be at least 3 characters";
|
||||||
|
}
|
||||||
|
else if (username.Length > 30)
|
||||||
|
{
|
||||||
|
_usernameFormatError = "Username must be 30 characters or fewer";
|
||||||
|
}
|
||||||
|
else if (username.Length >= 3 && !UsernamePattern.IsMatch(username))
|
||||||
|
{
|
||||||
|
_usernameFormatError = "Only letters, numbers, hyphens and underscores allowed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnUsernameBlur()
|
||||||
|
{
|
||||||
|
var username = _application.DesiredUsername?.Trim() ?? "";
|
||||||
|
|
||||||
|
// Don't check if empty, invalid format, or already checked this exact username
|
||||||
|
if (string.IsNullOrEmpty(username) || !UsernamePattern.IsMatch(username))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (username == _lastCheckedUsername && _usernameCheckState != UsernameCheckState.Error)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_usernameCheckState = UsernameCheckState.Checking;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
var available = await ApplicationService.CheckUsernameAsync(username);
|
||||||
|
_lastCheckedUsername = username;
|
||||||
|
|
||||||
|
_usernameCheckState = available switch
|
||||||
|
{
|
||||||
|
true => UsernameCheckState.Available,
|
||||||
|
false => UsernameCheckState.Taken,
|
||||||
|
null => UsernameCheckState.Error
|
||||||
|
};
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSubmit()
|
||||||
|
{
|
||||||
|
_errorMessage = null;
|
||||||
|
_submitting = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (success, message, token) = await ApplicationService.SubmitApplicationAsync(_application);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
_resultMessage = message;
|
||||||
|
_submitted = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_errorMessage = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_errorMessage = "An unexpected error occurred. Please try again later.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
BlazorApp/Components/Pages/Error.razor
Normal file
36
BlazorApp/Components/Pages/Error.razor
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
@page "/Error"
|
||||||
|
@using System.Diagnostics
|
||||||
|
|
||||||
|
<PageTitle>Error</PageTitle>
|
||||||
|
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
@if (ShowRequestId)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Development Mode</h3>
|
||||||
|
<p>
|
||||||
|
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||||
|
It can result in displaying sensitive information from exceptions to end users.
|
||||||
|
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||||
|
and restarting the app.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@code{
|
||||||
|
[CascadingParameter]
|
||||||
|
private HttpContext? HttpContext { get; set; }
|
||||||
|
|
||||||
|
private string? RequestId { get; set; }
|
||||||
|
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
|
||||||
|
protected override void OnInitialized() =>
|
||||||
|
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||||
|
}
|
||||||
79
BlazorApp/Components/Pages/Home.razor
Normal file
79
BlazorApp/Components/Pages/Home.razor
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
@page "/"
|
||||||
|
|
||||||
|
<PageTitle>SilverLabs - Innovation Gateway</PageTitle>
|
||||||
|
|
||||||
|
<div class="main-content visible">
|
||||||
|
<header class="header">
|
||||||
|
<img src="logo.png" alt="SilverLabs Logo" class="logo">
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<h1 class="title">Welcome to SilverLabs</h1>
|
||||||
|
<p class="subtitle">Your Innovation Gateway</p>
|
||||||
|
|
||||||
|
<div class="gateway-grid">
|
||||||
|
<a href="https://silverdesk.silverlabs.uk" class="gateway-card">
|
||||||
|
<div class="card-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="card-title">Help Desk</h2>
|
||||||
|
<p class="card-description">Support & Assistance</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://appstore.silverlabs.uk" class="gateway-card">
|
||||||
|
<div class="card-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="card-title">App Store</h2>
|
||||||
|
<p class="card-description">Applications & Tools</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://cloud.silverlabs.uk" class="gateway-card">
|
||||||
|
<div class="card-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="card-title">Cloud</h2>
|
||||||
|
<p class="card-description">Storage & Collaboration</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/sdk" class="gateway-card">
|
||||||
|
<div class="card-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="16 18 22 12 16 6"></polyline>
|
||||||
|
<polyline points="8 6 2 12 8 18"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="card-title">SDK</h2>
|
||||||
|
<p class="card-description">Developer Resources</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/developers" class="gateway-card">
|
||||||
|
<div class="card-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||||
|
<circle cx="9" cy="7" r="4"></circle>
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="card-title">Developers</h2>
|
||||||
|
<p class="card-description">Join the Team</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>© 2025 SilverLabs. All rights reserved.</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
237
BlazorApp/Components/Pages/Sdk.razor
Normal file
237
BlazorApp/Components/Pages/Sdk.razor
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
@page "/sdk"
|
||||||
|
|
||||||
|
<PageTitle>SilverSHELL SDK - SilverLabs</PageTitle>
|
||||||
|
|
||||||
|
<div class="main-content visible">
|
||||||
|
<header class="header">
|
||||||
|
<img src="../logo.png" alt="SilverLabs Logo" class="logo">
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="sdk-container">
|
||||||
|
<div class="sdk-header">
|
||||||
|
<h1>SilverSHELL SDK</h1>
|
||||||
|
<p style="font-size: 1.2rem; color: rgba(255, 255, 255, 0.7);">Build modular Blazor WebAssembly applications with ease</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Start Section -->
|
||||||
|
<div class="sdk-section">
|
||||||
|
<h2>🚀 Quick Start</h2>
|
||||||
|
<p>Get started with SilverSHELL in just a few minutes. Follow these simple steps:</p>
|
||||||
|
|
||||||
|
<ol class="steps-list">
|
||||||
|
<li>
|
||||||
|
<strong>Install the templates</strong> from NuGet
|
||||||
|
<div class="code-block"><code>dotnet new install SilverLabs.SilverSHELL.Templates</code></div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Create your first project</strong>
|
||||||
|
<div class="code-block"><code>dotnet new silvershell-starter -n MyApp --pwa true --module-repository true
|
||||||
|
cd MyApp
|
||||||
|
dotnet run</code></div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Access your application</strong> at <code>https://localhost:5001</code>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Downloads Section -->
|
||||||
|
<div class="sdk-section">
|
||||||
|
<h2>📦 Templates Package</h2>
|
||||||
|
<p>Install the SilverSHELL templates package from NuGet:</p>
|
||||||
|
|
||||||
|
<div class="download-grid">
|
||||||
|
<div class="download-card">
|
||||||
|
<h3>SilverSHELL Templates</h3>
|
||||||
|
<p>Complete package with both Starter and Module templates</p>
|
||||||
|
<p><strong>Package ID:</strong> SilverLabs.SilverSHELL.Templates</p>
|
||||||
|
<p><strong>Includes:</strong> Blazor WebAssembly starter template, module template with CI/CD, PWA support, module repository integration</p>
|
||||||
|
<div class="code-block"><code>dotnet new install SilverLabs.SilverSHELL.Templates</code></div>
|
||||||
|
<a href="https://nuget.silverlabs.uk/packages/silverlabs.silvershell.templates/" class="download-btn" target="_blank">
|
||||||
|
View on NuGet
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Creating Applications Section -->
|
||||||
|
<div class="sdk-section">
|
||||||
|
<h2>🏗️ Creating Applications</h2>
|
||||||
|
|
||||||
|
<h3>Using the Starter Template</h3>
|
||||||
|
<div class="code-block"><code># Create a new application
|
||||||
|
dotnet new silvershell-starter -n MyAwesomeApp
|
||||||
|
|
||||||
|
# With PWA support
|
||||||
|
dotnet new silvershell-starter -n MyAwesomeApp --pwa true
|
||||||
|
|
||||||
|
# With Module Repository integration
|
||||||
|
dotnet new silvershell-starter -n MyAwesomeApp --module-repository true</code></div>
|
||||||
|
|
||||||
|
<h3>Adding Modules</h3>
|
||||||
|
<p>SilverSHELL supports multiple ways to add modules to your application:</p>
|
||||||
|
|
||||||
|
<h3>Option 1: Configuration File</h3>
|
||||||
|
<p>Edit <code>wwwroot/appsettings.json</code>:</p>
|
||||||
|
<div class="code-block"><code>{
|
||||||
|
"AMS": {
|
||||||
|
"Deployment": {
|
||||||
|
"PreloadModules": [
|
||||||
|
"SilverLabs.SilverSHELL.Auth.Login",
|
||||||
|
"SilverSHELL.Modules.ModuleBrowser"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}</code></div>
|
||||||
|
|
||||||
|
<h3>Option 2: Module Browser UI</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Navigate to <code>/modules/browse</code> in your application</li>
|
||||||
|
<li>Search for modules from <strong>library.silverlabs.uk</strong></li>
|
||||||
|
<li>Click "Install" on any module</li>
|
||||||
|
<li>Modules are downloaded and installed automatically</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Option 3: Manual Installation</h3>
|
||||||
|
<div class="code-block"><code># Copy module DLLs to the modules directory
|
||||||
|
cp SomeModule.dll wwwroot/modules/
|
||||||
|
dotnet run
|
||||||
|
# Module is automatically discovered and loaded!</code></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Creating Modules Section -->
|
||||||
|
<div class="sdk-section">
|
||||||
|
<h2>🔧 Creating Modules</h2>
|
||||||
|
|
||||||
|
<h3>Using the Module Template</h3>
|
||||||
|
<div class="code-block"><code># Create a basic module
|
||||||
|
dotnet new silvershell-module -n MyModule
|
||||||
|
|
||||||
|
# Create a module with widgets
|
||||||
|
dotnet new silvershell-module -n MyModule --includeWidgets true
|
||||||
|
|
||||||
|
# Create a module with search provider
|
||||||
|
dotnet new silvershell-module -n MyModule --includeSearchProvider true
|
||||||
|
|
||||||
|
# Create a module with everything
|
||||||
|
dotnet new silvershell-module -n MyModule \
|
||||||
|
--includeWidgets true \
|
||||||
|
--includeSearchProvider true \
|
||||||
|
--includeTests true</code></div>
|
||||||
|
|
||||||
|
<h3>Module Structure</h3>
|
||||||
|
<p>The template creates an organized structure:</p>
|
||||||
|
<div class="code-block"><code>MyModule/
|
||||||
|
├── Configuration/
|
||||||
|
│ ├── ModuleMetadata.cs # Module identity and version
|
||||||
|
│ ├── EndpointConfiguration.cs # Navigation routes
|
||||||
|
│ └── WidgetConfiguration.cs # Dashboard widgets
|
||||||
|
├── Pages/
|
||||||
|
│ └── Index.razor # Razor pages
|
||||||
|
├── Components/
|
||||||
|
│ └── ... # Razor components
|
||||||
|
├── .gitlab-ci.yml # GitLab CI/CD
|
||||||
|
├── .github/workflows/
|
||||||
|
│ └── publish.yml # GitHub Actions
|
||||||
|
└── MyModuleMain.cs # Module entry point</code></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Publishing Modules Section -->
|
||||||
|
<div class="sdk-section">
|
||||||
|
<h2>🚀 Publishing Modules</h2>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<strong>CI/CD Included!</strong> The module template includes ready-to-use CI/CD pipelines for both GitLab and GitHub.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Automated Publishing (Recommended)</h3>
|
||||||
|
<p>The templates include CI/CD pipelines for automatic publishing:</p>
|
||||||
|
|
||||||
|
<ol class="steps-list">
|
||||||
|
<li>
|
||||||
|
<strong>Configure CI/CD variables</strong>
|
||||||
|
<div class="code-block"><code># In GitLab: Settings > CI/CD > Variables
|
||||||
|
# In GitHub: Settings > Secrets > Actions
|
||||||
|
|
||||||
|
# Add variable:
|
||||||
|
MODULE_REPO_TOKEN: [your token from library.silverlabs.uk]</code></div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Commit and push your code</strong>
|
||||||
|
<div class="code-block"><code>git add .
|
||||||
|
git commit -m "feat: Initial module implementation"
|
||||||
|
git push</code></div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Create a release tag</strong>
|
||||||
|
<div class="code-block"><code>git tag v1.0.0
|
||||||
|
git push --tags</code></div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Trigger publish</strong> from your CI/CD pipeline UI
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Manual Publishing</h3>
|
||||||
|
<div class="code-block"><code># Build and package
|
||||||
|
dotnet pack --configuration Release -o dist/
|
||||||
|
|
||||||
|
# Upload to repository
|
||||||
|
curl -X POST "https://library.silverlabs.uk/api/modules/publish" \
|
||||||
|
-F "id=MyModule" \
|
||||||
|
-F "name=My Awesome Module" \
|
||||||
|
-F "version=1.0.0" \
|
||||||
|
-F "description=A great module" \
|
||||||
|
-F "author=Your Name" \
|
||||||
|
-F "package=@@dist/MyModule.1.0.0.nupkg"</code></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Available Modules Section -->
|
||||||
|
<div class="sdk-section">
|
||||||
|
<h2>📚 Available Modules</h2>
|
||||||
|
<p>Browse and install modules from the SilverSHELL module repository:</p>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<strong>Module Repository:</strong> <a href="https://library.silverlabs.uk" target="_blank" style="color: #a78bfa;">library.silverlabs.uk</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Featured Modules:</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Auth.Login</strong> - User authentication and login UI</li>
|
||||||
|
<li><strong>Auth.Registration</strong> - User registration system</li>
|
||||||
|
<li><strong>Auth.UserManagement</strong> - User administration</li>
|
||||||
|
<li><strong>Auth.MyAccount</strong> - User profile management</li>
|
||||||
|
<li><strong>ModuleBrowser</strong> - Browse and install modules from the UI</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Explore All Modules</h3>
|
||||||
|
<div class="code-block"><code># List all available modules via API
|
||||||
|
curl https://library.silverlabs.uk/api/modules
|
||||||
|
|
||||||
|
# Search for specific modules
|
||||||
|
curl https://library.silverlabs.uk/api/modules/search?q=auth</code></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resources Section -->
|
||||||
|
<div class="sdk-section">
|
||||||
|
<h2>📖 Resources</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Module Repository API:</strong> <a href="https://library.silverlabs.uk/api/modules" target="_blank" style="color: #a78bfa;">https://library.silverlabs.uk/api/modules</a></li>
|
||||||
|
<li><strong>Demo Application:</strong> <a href="https://demo.silverlabs.uk" target="_blank" style="color: #a78bfa;">https://demo.silverlabs.uk</a></li>
|
||||||
|
<li><strong>GitLab Repository:</strong> <a href="https://gitlab.silverlabs.uk/silverlabs/silvershell" target="_blank" style="color: #a78bfa;">GitLab</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Support Section -->
|
||||||
|
<div class="sdk-section">
|
||||||
|
<h2>💬 Support</h2>
|
||||||
|
<p>Need help? We're here for you:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Help Desk:</strong> <a href="https://silverdesk.silverlabs.uk" target="_blank" style="color: #a78bfa;">silverdesk.silverlabs.uk</a></li>
|
||||||
|
<li><strong>Issues:</strong> <a href="https://gitlab.silverlabs.uk/silverlabs/silvershell/-/issues" target="_blank" style="color: #a78bfa;">GitLab Issues</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/" class="back-link">← Back to SilverLabs Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
64
BlazorApp/Components/Pages/Weather.razor
Normal file
64
BlazorApp/Components/Pages/Weather.razor
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
@page "/weather"
|
||||||
|
@attribute [StreamRendering]
|
||||||
|
|
||||||
|
<PageTitle>Weather</PageTitle>
|
||||||
|
|
||||||
|
<h1>Weather</h1>
|
||||||
|
|
||||||
|
<p>This component demonstrates showing data.</p>
|
||||||
|
|
||||||
|
@if (forecasts == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th aria-label="Temperature in Celsius">Temp. (C)</th>
|
||||||
|
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
|
||||||
|
<th>Summary</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var forecast in forecasts)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@forecast.Date.ToShortDateString()</td>
|
||||||
|
<td>@forecast.TemperatureC</td>
|
||||||
|
<td>@forecast.TemperatureF</td>
|
||||||
|
<td>@forecast.Summary</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private WeatherForecast[]? forecasts;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
// Simulate asynchronous loading to demonstrate streaming rendering
|
||||||
|
await Task.Delay(500);
|
||||||
|
|
||||||
|
var startDate = DateOnly.FromDateTime(DateTime.Now);
|
||||||
|
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
|
||||||
|
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||||
|
{
|
||||||
|
Date = startDate.AddDays(index),
|
||||||
|
TemperatureC = Random.Shared.Next(-20, 55),
|
||||||
|
Summary = summaries[Random.Shared.Next(summaries.Length)]
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class WeatherForecast
|
||||||
|
{
|
||||||
|
public DateOnly Date { get; set; }
|
||||||
|
public int TemperatureC { get; set; }
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
BlazorApp/Components/Routes.razor
Normal file
6
BlazorApp/Components/Routes.razor
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<Router AppAssembly="typeof(Program).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
</Router>
|
||||||
10
BlazorApp/Components/_Imports.razor
Normal file
10
BlazorApp/Components/_Imports.razor
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using SilverLabs.Website
|
||||||
|
@using SilverLabs.Website.Components
|
||||||
188
BlazorApp/Endpoints/DeveloperEndpoints.cs
Normal file
188
BlazorApp/Endpoints/DeveloperEndpoints.cs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
using SilverLabs.Website.Models;
|
||||||
|
using SilverLabs.Website.Services;
|
||||||
|
|
||||||
|
namespace SilverLabs.Website.Endpoints;
|
||||||
|
|
||||||
|
public static class DeveloperEndpoints
|
||||||
|
{
|
||||||
|
public static void MapDeveloperEndpoints(this WebApplication app)
|
||||||
|
{
|
||||||
|
var group = app.MapGroup("/api/developers");
|
||||||
|
|
||||||
|
group.MapGet("/check-username/{username}", async (string username, DeveloperApplicationService service) =>
|
||||||
|
{
|
||||||
|
var available = await service.CheckUsernameAsync(username);
|
||||||
|
if (available is null)
|
||||||
|
return Results.Problem("Unable to verify username availability", statusCode: 503);
|
||||||
|
return Results.Ok(new { available = available.Value });
|
||||||
|
});
|
||||||
|
|
||||||
|
group.MapPost("/apply", async (DeveloperApplication application, DeveloperApplicationService service) =>
|
||||||
|
{
|
||||||
|
var (success, message, token) = await service.SubmitApplicationAsync(application);
|
||||||
|
return success
|
||||||
|
? Results.Ok(new { message, token })
|
||||||
|
: Results.Problem(message, statusCode: 502);
|
||||||
|
});
|
||||||
|
|
||||||
|
group.MapPost("/approve/{ticketId}", async (
|
||||||
|
string ticketId,
|
||||||
|
DeveloperTicketParsingService ticketService,
|
||||||
|
ProvisioningService provisioningService,
|
||||||
|
HttpContext context,
|
||||||
|
IConfiguration config) =>
|
||||||
|
{
|
||||||
|
var apiKey = context.Request.Headers["X-Api-Key"].FirstOrDefault();
|
||||||
|
var expectedKey = config["AdminApiKey"];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(expectedKey) || apiKey != expectedKey)
|
||||||
|
return Results.Unauthorized();
|
||||||
|
|
||||||
|
var ticket = await ticketService.FetchTicketAsync(ticketId);
|
||||||
|
if (ticket is null)
|
||||||
|
return Results.Problem("Failed to fetch ticket from SilverDESK", statusCode: 502);
|
||||||
|
|
||||||
|
var description = ticket.Value.GetProperty("description").GetString() ?? "";
|
||||||
|
var (fullName, email, desiredUsername, role) = ticketService.ParseApplicationFromDescription(description);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(fullName) || string.IsNullOrEmpty(email) || string.IsNullOrEmpty(desiredUsername))
|
||||||
|
return Results.Problem("Could not parse applicant details from ticket description", statusCode: 422);
|
||||||
|
|
||||||
|
// Generate confirmation token instead of provisioning immediately
|
||||||
|
var deployment = provisioningService.CreatePendingDeployment(desiredUsername, email, fullName, ticketId, role);
|
||||||
|
|
||||||
|
var siteBase = config["SiteBaseUrl"] ?? "https://silverlabs.uk";
|
||||||
|
var confirmUrl = $"{siteBase}/developers/confirm/{deployment.Token}";
|
||||||
|
|
||||||
|
// Send ticket reply with confirmation link
|
||||||
|
var giteaLine = string.Equals(role, "Developer", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "\n- **Gitea**: Source code repository access"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
var replyContent = $"""
|
||||||
|
Your application has been approved! To activate your accounts, please confirm your identity:
|
||||||
|
|
||||||
|
**[Click here to activate your accounts]({confirmUrl})**
|
||||||
|
|
||||||
|
You'll need to enter your SilverDESK password to complete the setup. This link expires in 48 hours.
|
||||||
|
|
||||||
|
Once confirmed, the following accounts will be created for you:
|
||||||
|
- **Email**: {desiredUsername}@silverlabs.uk
|
||||||
|
- **Mattermost**: Team chat access{giteaLine}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var (replyOk, replyMsg) = await provisioningService.SendTicketReplyAsync(ticketId, replyContent);
|
||||||
|
|
||||||
|
return Results.Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = $"Confirmation link generated and sent via ticket reply. Reply status: {replyMsg}",
|
||||||
|
confirmUrl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Token info endpoint for the confirmation page
|
||||||
|
group.MapGet("/deployment-info/{token}", (string token, ProvisioningService provisioningService) =>
|
||||||
|
{
|
||||||
|
var deployment = provisioningService.GetPendingDeployment(token);
|
||||||
|
if (deployment is null)
|
||||||
|
return Results.NotFound(new { message = "Invalid or expired confirmation link" });
|
||||||
|
|
||||||
|
return Results.Ok(new
|
||||||
|
{
|
||||||
|
username = deployment.Username,
|
||||||
|
email = deployment.Email,
|
||||||
|
fullName = deployment.FullName,
|
||||||
|
expiresAt = deployment.ExpiresAt
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm deployment with password
|
||||||
|
group.MapPost("/confirm-deployment", async (
|
||||||
|
ConfirmDeploymentRequest request,
|
||||||
|
ProvisioningService provisioningService) =>
|
||||||
|
{
|
||||||
|
var deployment = provisioningService.GetPendingDeployment(request.Token);
|
||||||
|
if (deployment is null)
|
||||||
|
return Results.NotFound(new { message = "Invalid or expired confirmation link" });
|
||||||
|
|
||||||
|
// Validate credentials against SilverDESK
|
||||||
|
var authenticated = await provisioningService.ValidateSilverDeskCredentialsAsync(
|
||||||
|
deployment.Username, request.Password);
|
||||||
|
|
||||||
|
if (!authenticated)
|
||||||
|
return Results.Json(new { message = "Invalid password. Please enter your SilverDESK password." }, statusCode: 401);
|
||||||
|
|
||||||
|
// Provision all services with the user's password
|
||||||
|
var (success, message) = await provisioningService.ProvisionWithPasswordAsync(
|
||||||
|
deployment.TicketId, deployment.Username, deployment.Email, deployment.FullName, request.Password, deployment.Role);
|
||||||
|
|
||||||
|
var isDeveloper = string.Equals(deployment.Role, "Developer", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var giteaSuccessSection = isDeveloper
|
||||||
|
? $"\n\n**Gitea** (Source Code): [git.silverlabs.uk](https://git.silverlabs.uk)"
|
||||||
|
: "";
|
||||||
|
var giteaFailSection = isDeveloper
|
||||||
|
? $"\n- **Gitea**: [git.silverlabs.uk](https://git.silverlabs.uk)"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// Send follow-up ticket reply with results
|
||||||
|
var resultContent = success
|
||||||
|
? $"""
|
||||||
|
Your accounts have been successfully provisioned! Here's how to access your services:
|
||||||
|
|
||||||
|
**Email**: {deployment.Username}@silverlabs.uk
|
||||||
|
- Webmail: [mail.silverlined.uk](https://mail.silverlined.uk)
|
||||||
|
- IMAP: `mail.silverlined.uk:993` (SSL)
|
||||||
|
- SMTP: `mail.silverlined.uk:465` (SSL)
|
||||||
|
|
||||||
|
**Mattermost** (Team Chat): [ops.silverlined.uk](https://ops.silverlined.uk){giteaSuccessSection}
|
||||||
|
|
||||||
|
**SilverDESK** (Support & Tickets): [silverdesk.silverlabs.uk](https://silverdesk.silverlabs.uk)
|
||||||
|
|
||||||
|
All services use the same password you entered during activation.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Provisioning status: {message}*
|
||||||
|
"""
|
||||||
|
: $"""
|
||||||
|
Account provisioning completed with some issues:
|
||||||
|
|
||||||
|
{message}
|
||||||
|
|
||||||
|
Some services may not be available yet. Please contact an administrator for assistance.
|
||||||
|
|
||||||
|
Once resolved, your services will be:
|
||||||
|
- **Email**: {deployment.Username}@silverlabs.uk — [mail.silverlined.uk](https://mail.silverlined.uk)
|
||||||
|
- **Mattermost**: [ops.silverlined.uk](https://ops.silverlined.uk){giteaFailSection}
|
||||||
|
""";
|
||||||
|
|
||||||
|
await provisioningService.SendTicketReplyAsync(deployment.TicketId, resultContent, "close");
|
||||||
|
|
||||||
|
// Remove the used token
|
||||||
|
provisioningService.RemovePendingDeployment(request.Token);
|
||||||
|
|
||||||
|
return Results.Ok(new { success, message });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Password sync endpoint (called by SilverDESK on password reset)
|
||||||
|
group.MapPost("/sync-password", async (
|
||||||
|
SyncPasswordRequest request,
|
||||||
|
ProvisioningService provisioningService,
|
||||||
|
HttpContext context,
|
||||||
|
IConfiguration config) =>
|
||||||
|
{
|
||||||
|
var apiKey = context.Request.Headers["X-Api-Key"].FirstOrDefault();
|
||||||
|
var expectedKey = config["AdminApiKey"];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(expectedKey) || apiKey != expectedKey)
|
||||||
|
return Results.Unauthorized();
|
||||||
|
|
||||||
|
var (success, message) = await provisioningService.SyncPasswordAsync(request.Username, request.NewPassword);
|
||||||
|
|
||||||
|
return Results.Ok(new { success, message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ConfirmDeploymentRequest(string Token, string Password);
|
||||||
|
public record SyncPasswordRequest(string Username, string NewPassword);
|
||||||
72
BlazorApp/Models/DeveloperApplication.cs
Normal file
72
BlazorApp/Models/DeveloperApplication.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace SilverLabs.Website.Models;
|
||||||
|
|
||||||
|
public class DeveloperApplication : IValidatableObject
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "Full name is required")]
|
||||||
|
[StringLength(100, MinimumLength = 2)]
|
||||||
|
public string FullName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[EmailAddress(ErrorMessage = "Invalid email address")]
|
||||||
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Username is required")]
|
||||||
|
[RegularExpression(@"^[a-zA-Z0-9_-]{3,30}$", ErrorMessage = "Username must be 3-30 characters, letters, numbers, hyphens and underscores only")]
|
||||||
|
public string DesiredUsername { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Timezone is required")]
|
||||||
|
public string Timezone { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please select a role")]
|
||||||
|
public ApplicationRole Role { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please select at least one platform")]
|
||||||
|
[MinLength(1, ErrorMessage = "Please select at least one platform")]
|
||||||
|
public List<string> Platforms { get; set; } = new();
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Password is required")]
|
||||||
|
[StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")]
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please confirm your password")]
|
||||||
|
[Compare("Password", ErrorMessage = "Passwords do not match")]
|
||||||
|
public string ConfirmPassword { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Tester-specific
|
||||||
|
public int? InternetUnderstanding { get; set; }
|
||||||
|
public int? EnjoysTesting { get; set; }
|
||||||
|
|
||||||
|
// Developer-specific
|
||||||
|
public string? ExperienceRange { get; set; }
|
||||||
|
public List<string> SelectedSkills { get; set; } = new();
|
||||||
|
|
||||||
|
// Shared optional
|
||||||
|
public string? AdditionalNotes { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (Role == ApplicationRole.Tester)
|
||||||
|
{
|
||||||
|
if (!InternetUnderstanding.HasValue || InternetUnderstanding < 1 || InternetUnderstanding > 5)
|
||||||
|
yield return new ValidationResult("Please rate your internet understanding", new[] { nameof(InternetUnderstanding) });
|
||||||
|
|
||||||
|
if (!EnjoysTesting.HasValue || EnjoysTesting < 1 || EnjoysTesting > 5)
|
||||||
|
yield return new ValidationResult("Please rate your enthusiasm for testing", new[] { nameof(EnjoysTesting) });
|
||||||
|
}
|
||||||
|
else if (Role == ApplicationRole.Developer)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ExperienceRange))
|
||||||
|
yield return new ValidationResult("Please select your experience level", new[] { nameof(ExperienceRange) });
|
||||||
|
|
||||||
|
if (SelectedSkills.Count == 0)
|
||||||
|
yield return new ValidationResult("Please select at least one skill", new[] { nameof(SelectedSkills) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ApplicationRole
|
||||||
|
{
|
||||||
|
Tester,
|
||||||
|
Developer
|
||||||
|
}
|
||||||
37
BlazorApp/Models/SkillCatalog.cs
Normal file
37
BlazorApp/Models/SkillCatalog.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace SilverLabs.Website.Models;
|
||||||
|
|
||||||
|
public static class SkillCatalog
|
||||||
|
{
|
||||||
|
public static readonly string[] ExperienceRanges =
|
||||||
|
{
|
||||||
|
"< 1 year",
|
||||||
|
"1-3 years",
|
||||||
|
"3-5 years",
|
||||||
|
"5-10 years",
|
||||||
|
"10+ years"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Dictionary<string, string[]> SkillCategories = new()
|
||||||
|
{
|
||||||
|
["Languages"] = new[]
|
||||||
|
{
|
||||||
|
"C#", "Python", "JavaScript", "TypeScript", "Go", "Rust",
|
||||||
|
"Java", "C/C++", "PHP", "Ruby", "Swift", "Kotlin"
|
||||||
|
},
|
||||||
|
["Frameworks"] = new[]
|
||||||
|
{
|
||||||
|
".NET/Blazor", "React", "Angular", "Vue", "Django",
|
||||||
|
"Node.js", "Next.js", "Svelte", "Spring Boot", "Flask"
|
||||||
|
},
|
||||||
|
["Infrastructure"] = new[]
|
||||||
|
{
|
||||||
|
"Docker", "Kubernetes", "Linux", "Nginx", "Terraform",
|
||||||
|
"CI/CD", "AWS", "Azure", "Proxmox"
|
||||||
|
},
|
||||||
|
["Databases"] = new[]
|
||||||
|
{
|
||||||
|
"PostgreSQL", "MySQL", "SQLite", "MongoDB", "Redis",
|
||||||
|
"SQL Server", "Elasticsearch"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
84
BlazorApp/Program.cs
Normal file
84
BlazorApp/Program.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using SilverLabs.Website.Components;
|
||||||
|
using SilverLabs.Website.Endpoints;
|
||||||
|
using SilverLabs.Website.Services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
builder.Services.AddRazorComponents()
|
||||||
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
|
// HttpClient for SilverDESK (used by DeveloperApplicationService directly)
|
||||||
|
builder.Services.AddHttpClient<DeveloperApplicationService>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(builder.Configuration["SilverDesk:BaseUrl"] ?? "https://silverdesk.silverlabs.uk");
|
||||||
|
var apiKey = builder.Configuration["SilverDesk:ApiKey"];
|
||||||
|
if (!string.IsNullOrEmpty(apiKey))
|
||||||
|
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
// HttpClient for DeveloperTicketParsingService (fetches tickets from SilverDESK)
|
||||||
|
builder.Services.AddHttpClient<DeveloperTicketParsingService>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(builder.Configuration["SilverDesk:BaseUrl"] ?? "https://silverdesk.silverlabs.uk");
|
||||||
|
var apiKey = builder.Configuration["SilverDesk:ApiKey"];
|
||||||
|
if (!string.IsNullOrEmpty(apiKey))
|
||||||
|
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Named HttpClients for provisioning
|
||||||
|
builder.Services.AddHttpClient("SilverDesk", client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(builder.Configuration["SilverDesk:BaseUrl"] ?? "https://silverdesk.silverlabs.uk");
|
||||||
|
var apiKey = builder.Configuration["SilverDesk:ApiKey"];
|
||||||
|
if (!string.IsNullOrEmpty(apiKey))
|
||||||
|
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient("Mattermost", client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(builder.Configuration["Mattermost:BaseUrl"] ?? "https://ops.silverlined.uk");
|
||||||
|
var token = builder.Configuration["Mattermost:ApiToken"];
|
||||||
|
if (!string.IsNullOrEmpty(token))
|
||||||
|
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient("Mailcow", client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(builder.Configuration["Mailcow:BaseUrl"] ?? "https://mail.silverlined.uk");
|
||||||
|
var apiKey = builder.Configuration["Mailcow:ApiKey"];
|
||||||
|
if (!string.IsNullOrEmpty(apiKey))
|
||||||
|
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient("Gitea", client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(builder.Configuration["Gitea:BaseUrl"] ?? "https://git.silverlabs.uk");
|
||||||
|
var token = builder.Configuration["Gitea:ApiToken"];
|
||||||
|
if (!string.IsNullOrEmpty(token))
|
||||||
|
client.DefaultRequestHeaders.Add("Authorization", $"token {token}");
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<ProvisioningService>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||||
|
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||||
|
app.UseHsts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// app.UseHttpsRedirection(); // Disabled - running behind reverse proxy
|
||||||
|
|
||||||
|
app.UseAntiforgery();
|
||||||
|
|
||||||
|
app.MapStaticAssets();
|
||||||
|
app.MapRazorComponents<App>()
|
||||||
|
.AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
|
app.MapDeveloperEndpoints();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
23
BlazorApp/Properties/launchSettings.json
Normal file
23
BlazorApp/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:5072",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:7104;http://localhost:5072",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
285
BlazorApp/Services/DeveloperApplicationService.cs
Normal file
285
BlazorApp/Services/DeveloperApplicationService.cs
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using SilverLabs.Website.Models;
|
||||||
|
|
||||||
|
namespace SilverLabs.Website.Services;
|
||||||
|
|
||||||
|
public class DeveloperApplicationService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<DeveloperApplicationService> _logger;
|
||||||
|
|
||||||
|
public DeveloperApplicationService(HttpClient httpClient, ILogger<DeveloperApplicationService> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks username availability. Returns: true = available, false = taken, null = error/unknown.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool?> CheckUsernameAsync(string username)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync($"/api/auth/check-username/{Uri.EscapeDataString(username)}");
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Username check returned {StatusCode} for {Username}", response.StatusCode, username);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
if (result.TryGetProperty("available", out var available))
|
||||||
|
return available.GetBoolean();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error checking username availability for {Username}", username);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool Success, string Message, string? Token)> SubmitApplicationAsync(DeveloperApplication application)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Use silverlabs.uk address when no personal email provided
|
||||||
|
var effectiveEmail = string.IsNullOrWhiteSpace(application.Email)
|
||||||
|
? $"{application.DesiredUsername}@silverlabs.uk"
|
||||||
|
: application.Email.Trim();
|
||||||
|
|
||||||
|
// 1. Register user on SilverDESK
|
||||||
|
var registerPayload = new
|
||||||
|
{
|
||||||
|
username = application.DesiredUsername,
|
||||||
|
email = effectiveEmail,
|
||||||
|
password = application.Password,
|
||||||
|
fullName = application.FullName
|
||||||
|
};
|
||||||
|
|
||||||
|
var registerResponse = await _httpClient.PostAsJsonAsync("/api/auth/register", registerPayload);
|
||||||
|
|
||||||
|
if (!registerResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorBody = await registerResponse.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("SilverDESK registration failed: {StatusCode} - {Body}", registerResponse.StatusCode, errorBody);
|
||||||
|
|
||||||
|
var friendlyMessage = ParseRegistrationError(errorBody);
|
||||||
|
return (false, friendlyMessage, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var authResult = await registerResponse.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var token = authResult.GetProperty("token").GetString();
|
||||||
|
|
||||||
|
// 2. Create ticket using the user's own JWT
|
||||||
|
var ticketBody = FormatTicketBody(application);
|
||||||
|
var ticketPayload = new
|
||||||
|
{
|
||||||
|
Subject = $"[Developer Program] {application.Role} Application - {application.FullName}",
|
||||||
|
Description = ticketBody,
|
||||||
|
Priority = "Medium",
|
||||||
|
Category = "Developer Program"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use a fresh HttpClient without the X-API-Key default header so that
|
||||||
|
// SilverDESK's MultiAuth policy routes to Bearer/JWT auth (the new user's token)
|
||||||
|
// instead of ApiKey auth (which resolves to the MCP system user).
|
||||||
|
using var userClient = new HttpClient { BaseAddress = _httpClient.BaseAddress };
|
||||||
|
var ticketRequest = new HttpRequestMessage(HttpMethod.Post, "/api/tickets");
|
||||||
|
ticketRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
|
ticketRequest.Content = JsonContent.Create(ticketPayload);
|
||||||
|
|
||||||
|
var ticketResponse = await userClient.SendAsync(ticketRequest);
|
||||||
|
|
||||||
|
if (!ticketResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorBody = await ticketResponse.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Failed to create ticket: {StatusCode} - {Body}", ticketResponse.StatusCode, errorBody);
|
||||||
|
// User was created but ticket failed — still return success with a note
|
||||||
|
return (true, "Your account has been created, but we had trouble submitting your application ticket. Please log in to SilverDESK and create a support ticket.", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Create DeveloperApplication record linking user + ticket
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = authResult.GetProperty("user").GetProperty("id").GetString();
|
||||||
|
var ticketResult = await ticketResponse.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var ticketId = ticketResult.GetProperty("id").GetString();
|
||||||
|
|
||||||
|
var applicationPayload = new
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
ticketId,
|
||||||
|
fullName = application.FullName,
|
||||||
|
email = effectiveEmail,
|
||||||
|
desiredUsername = application.DesiredUsername,
|
||||||
|
timezone = application.Timezone,
|
||||||
|
appliedRole = application.Role.ToString(),
|
||||||
|
platforms = application.Platforms,
|
||||||
|
skills = SerializeAssessment(application),
|
||||||
|
motivation = GenerateMotivationSummary(application),
|
||||||
|
status = 0, // Pending
|
||||||
|
silverDeskProvisioned = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var appResponse = await _httpClient.PostAsJsonAsync("/api/developer-program/applications", applicationPayload);
|
||||||
|
|
||||||
|
if (appResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("DeveloperApplication record created for {Email}", effectiveEmail);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var appError = await appResponse.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Failed to create DeveloperApplication record for {Email}: {StatusCode} - {Body}",
|
||||||
|
effectiveEmail, appResponse.StatusCode, appError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to create DeveloperApplication record for {Email} — user and ticket were created successfully",
|
||||||
|
effectiveEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Developer application submitted for {Email} as {Role} — user registered and ticket created",
|
||||||
|
effectiveEmail, application.Role);
|
||||||
|
|
||||||
|
return (true, "Application submitted successfully! Your SilverDESK account has been created.", token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error submitting developer application for {Username}", application.DesiredUsername);
|
||||||
|
return (false, "Unable to connect to the application service. Please try again later.", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes structured assessment data as JSON for the Skills column.
|
||||||
|
/// </summary>
|
||||||
|
internal static string SerializeAssessment(DeveloperApplication app)
|
||||||
|
{
|
||||||
|
object data;
|
||||||
|
|
||||||
|
if (app.Role == ApplicationRole.Tester)
|
||||||
|
{
|
||||||
|
data = new
|
||||||
|
{
|
||||||
|
type = "tester",
|
||||||
|
internetUnderstanding = app.InternetUnderstanding ?? 0,
|
||||||
|
enjoysTesting = app.EnjoysTesting ?? 0,
|
||||||
|
additionalNotes = app.AdditionalNotes ?? ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = new
|
||||||
|
{
|
||||||
|
type = "developer",
|
||||||
|
experienceRange = app.ExperienceRange ?? "",
|
||||||
|
selectedSkills = app.SelectedSkills,
|
||||||
|
additionalNotes = app.AdditionalNotes ?? ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(data, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a human-readable summary for the Motivation field (backward compatibility).
|
||||||
|
/// </summary>
|
||||||
|
internal static string GenerateMotivationSummary(DeveloperApplication app)
|
||||||
|
{
|
||||||
|
if (app.Role == ApplicationRole.Tester)
|
||||||
|
{
|
||||||
|
var summary = $"Internet understanding: {app.InternetUnderstanding}/5, Testing enthusiasm: {app.EnjoysTesting}/5";
|
||||||
|
if (!string.IsNullOrWhiteSpace(app.AdditionalNotes))
|
||||||
|
summary += $". Notes: {app.AdditionalNotes.Trim()}";
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var skills = app.SelectedSkills.Count > 0
|
||||||
|
? string.Join(", ", app.SelectedSkills)
|
||||||
|
: "None selected";
|
||||||
|
var summary = $"{app.ExperienceRange} experience. Skills: {skills}";
|
||||||
|
if (!string.IsNullOrWhiteSpace(app.AdditionalNotes))
|
||||||
|
summary += $". Notes: {app.AdditionalNotes.Trim()}";
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ParseRegistrationError(string errorBody)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var error = JsonSerializer.Deserialize<JsonElement>(errorBody);
|
||||||
|
if (error.TryGetProperty("message", out var message))
|
||||||
|
{
|
||||||
|
var msg = message.GetString() ?? "";
|
||||||
|
if (msg.Contains("Username already exists", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "That username is already taken. Please choose a different one.";
|
||||||
|
if (msg.Contains("Email already exists", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "An account with that email already exists.";
|
||||||
|
if (msg.Contains("Password", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return msg;
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
return "Something went wrong creating your account. Please try again later.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatTicketBody(DeveloperApplication app)
|
||||||
|
{
|
||||||
|
var effectiveEmail = string.IsNullOrWhiteSpace(app.Email)
|
||||||
|
? $"{app.DesiredUsername}@silverlabs.uk"
|
||||||
|
: app.Email.Trim();
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("## Developer Program Application");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"**Role:** {app.Role}");
|
||||||
|
sb.AppendLine($"**Full Name:** {app.FullName}");
|
||||||
|
sb.AppendLine($"**Email:** {effectiveEmail}");
|
||||||
|
sb.AppendLine($"**Desired Username:** {app.DesiredUsername}");
|
||||||
|
sb.AppendLine($"**Timezone:** {app.Timezone}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"**Platforms:** {string.Join(", ", app.Platforms)}");
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
if (app.Role == ApplicationRole.Tester)
|
||||||
|
{
|
||||||
|
sb.AppendLine("### Assessment");
|
||||||
|
sb.AppendLine($"- Internet understanding: {"*".PadLeft(app.InternetUnderstanding ?? 0, '*')}{new string('-', 5 - (app.InternetUnderstanding ?? 0))} ({app.InternetUnderstanding}/5)");
|
||||||
|
sb.AppendLine($"- Testing enthusiasm: {"*".PadLeft(app.EnjoysTesting ?? 0, '*')}{new string('-', 5 - (app.EnjoysTesting ?? 0))} ({app.EnjoysTesting}/5)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.AppendLine("### Skills & Experience");
|
||||||
|
sb.AppendLine($"**Experience:** {app.ExperienceRange}");
|
||||||
|
sb.AppendLine();
|
||||||
|
if (app.SelectedSkills.Count > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"**Technologies:** {string.Join(", ", app.SelectedSkills)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(app.AdditionalNotes))
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("### Additional Notes");
|
||||||
|
sb.AppendLine(app.AdditionalNotes.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
BlazorApp/Services/DeveloperTicketParsingService.cs
Normal file
53
BlazorApp/Services/DeveloperTicketParsingService.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace SilverLabs.Website.Services;
|
||||||
|
|
||||||
|
public class DeveloperTicketParsingService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<DeveloperTicketParsingService> _logger;
|
||||||
|
|
||||||
|
public DeveloperTicketParsingService(HttpClient httpClient, ILogger<DeveloperTicketParsingService> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JsonElement?> FetchTicketAsync(string ticketId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync($"/api/tickets/{ticketId}");
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogError("Failed to fetch ticket {TicketId}: {Status}", ticketId, response.StatusCode);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
return JsonSerializer.Deserialize<JsonElement>(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error fetching ticket {TicketId}", ticketId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (string? FullName, string? Email, string? DesiredUsername, string? Role) ParseApplicationFromDescription(string description)
|
||||||
|
{
|
||||||
|
var fullName = ExtractField(description, @"\*\*Full Name:\*\*\s*(.+)");
|
||||||
|
var email = ExtractField(description, @"\*\*Email:\*\*\s*(.+)");
|
||||||
|
var desiredUsername = ExtractField(description, @"\*\*Desired Username:\*\*\s*(.+)");
|
||||||
|
var role = ExtractField(description, @"\*\*Role:\*\*\s*(.+)");
|
||||||
|
|
||||||
|
return (fullName, email, desiredUsername, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractField(string text, string pattern)
|
||||||
|
{
|
||||||
|
var match = Regex.Match(text, pattern);
|
||||||
|
return match.Success ? match.Groups[1].Value.Trim() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
550
BlazorApp/Services/ProvisioningService.cs
Normal file
550
BlazorApp/Services/ProvisioningService.cs
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace SilverLabs.Website.Services;
|
||||||
|
|
||||||
|
public record PendingDeployment(
|
||||||
|
string Token,
|
||||||
|
string Username,
|
||||||
|
string Email,
|
||||||
|
string FullName,
|
||||||
|
string TicketId,
|
||||||
|
string? Role,
|
||||||
|
DateTime CreatedAt,
|
||||||
|
DateTime ExpiresAt);
|
||||||
|
|
||||||
|
public class ProvisioningService
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly ILogger<ProvisioningService> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<string, PendingDeployment> _pendingDeployments = new();
|
||||||
|
|
||||||
|
public ProvisioningService(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
ILogger<ProvisioningService> logger,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Token management ---
|
||||||
|
|
||||||
|
public PendingDeployment CreatePendingDeployment(string username, string email, string fullName, string ticketId, string? role = null)
|
||||||
|
{
|
||||||
|
CleanupExpiredTokens();
|
||||||
|
|
||||||
|
var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
|
||||||
|
.Replace("+", "-").Replace("/", "_").TrimEnd('=');
|
||||||
|
|
||||||
|
var deployment = new PendingDeployment(
|
||||||
|
token, username, email, fullName, ticketId, role,
|
||||||
|
DateTime.UtcNow, DateTime.UtcNow.AddHours(48));
|
||||||
|
|
||||||
|
_pendingDeployments[token] = deployment;
|
||||||
|
_logger.LogInformation("Created pending deployment for {Username} (ticket {TicketId}), token expires {ExpiresAt}",
|
||||||
|
username, ticketId, deployment.ExpiresAt);
|
||||||
|
|
||||||
|
return deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PendingDeployment? GetPendingDeployment(string token)
|
||||||
|
{
|
||||||
|
CleanupExpiredTokens();
|
||||||
|
|
||||||
|
if (_pendingDeployments.TryGetValue(token, out var deployment))
|
||||||
|
{
|
||||||
|
if (deployment.ExpiresAt > DateTime.UtcNow)
|
||||||
|
return deployment;
|
||||||
|
|
||||||
|
_pendingDeployments.TryRemove(token, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemovePendingDeployment(string token)
|
||||||
|
{
|
||||||
|
_pendingDeployments.TryRemove(token, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupExpiredTokens()
|
||||||
|
{
|
||||||
|
var expired = _pendingDeployments
|
||||||
|
.Where(kvp => kvp.Value.ExpiresAt <= DateTime.UtcNow)
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var key in expired)
|
||||||
|
_pendingDeployments.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Authentication ---
|
||||||
|
|
||||||
|
public async Task<bool> ValidateSilverDeskCredentialsAsync(string username, string password)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("SilverDesk");
|
||||||
|
var payload = new { username, password };
|
||||||
|
var response = await client.PostAsJsonAsync("/api/auth/login", payload);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("SilverDESK credential validation succeeded for {Username}", username);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning("SilverDESK credential validation failed for {Username}: {Status}", username, response.StatusCode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error validating SilverDESK credentials for {Username}", username);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Full provisioning with password ---
|
||||||
|
|
||||||
|
public async Task<(bool Success, string Message)> ProvisionWithPasswordAsync(
|
||||||
|
string ticketId, string username, string email, string fullName, string password, string? role = null)
|
||||||
|
{
|
||||||
|
var results = new List<string>();
|
||||||
|
var allSuccess = true;
|
||||||
|
|
||||||
|
// 1. Create Mattermost user
|
||||||
|
var (mmOk, mmMsg) = await CreateMattermostUserAsync(username, email, fullName, password);
|
||||||
|
results.Add($"Mattermost: {mmMsg}");
|
||||||
|
if (!mmOk) allSuccess = false;
|
||||||
|
|
||||||
|
// 1b. Add to SilverLABS team (only if user was created)
|
||||||
|
if (mmOk)
|
||||||
|
{
|
||||||
|
var (teamOk, teamMsg) = await AddMattermostUserToTeamAsync(username);
|
||||||
|
results.Add($"Mattermost Team: {teamMsg}");
|
||||||
|
if (!teamOk) allSuccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create Mailcow mailbox
|
||||||
|
var (mailOk, mailMsg) = await CreateMailcowMailboxAsync(username, fullName, password);
|
||||||
|
results.Add($"Mailcow: {mailMsg}");
|
||||||
|
if (!mailOk) allSuccess = false;
|
||||||
|
|
||||||
|
// 3. Create Gitea user (Developers only)
|
||||||
|
var giteaOk = false;
|
||||||
|
if (string.Equals(role, "Developer", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var (gOk, giteaMsg) = await CreateGiteaUserAsync(username, email, fullName, password);
|
||||||
|
giteaOk = gOk;
|
||||||
|
results.Add($"Gitea: {giteaMsg}");
|
||||||
|
if (!giteaOk) allSuccess = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
results.Add("Gitea: Skipped (not required for Tester role)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Update the DeveloperApplication record in SilverDESK
|
||||||
|
var (updateOk, updateMsg) = await UpdateApplicationStatusAsync(ticketId, mmOk, mailOk, giteaOk);
|
||||||
|
results.Add($"Application record: {updateMsg}");
|
||||||
|
if (!updateOk) allSuccess = false;
|
||||||
|
|
||||||
|
var summary = string.Join("; ", results);
|
||||||
|
_logger.LogInformation("Provisioning for {Username} (ticket {TicketId}): {Summary}", username, ticketId, summary);
|
||||||
|
|
||||||
|
return (allSuccess, summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Ticket replies ---
|
||||||
|
|
||||||
|
public async Task<(bool Success, string Message)> SendTicketReplyAsync(string ticketId, string content, string action = "waitingcustomer")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("SilverDesk");
|
||||||
|
var payload = new { content, action };
|
||||||
|
var response = await client.PostAsJsonAsync($"/api/tickets/{ticketId}/reply", payload);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Sent ticket reply to {TicketId} with action {Action}", ticketId, action);
|
||||||
|
return (true, "Reply sent");
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Failed to send ticket reply to {TicketId}: {Status} {Body}", ticketId, response.StatusCode, body);
|
||||||
|
return (false, $"Failed ({response.StatusCode})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error sending ticket reply to {TicketId}", ticketId);
|
||||||
|
return (false, $"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Password sync ---
|
||||||
|
|
||||||
|
public async Task<(bool Success, string Message)> SyncPasswordAsync(string username, string newPassword)
|
||||||
|
{
|
||||||
|
// Normalize username to lowercase - Mattermost and Gitea store usernames as lowercase
|
||||||
|
// and their API lookups are case-sensitive
|
||||||
|
var normalizedUsername = username.ToLowerInvariant();
|
||||||
|
var results = new List<string>();
|
||||||
|
var allSuccess = true;
|
||||||
|
|
||||||
|
// 1. Mattermost - need to look up user ID first
|
||||||
|
var (mmOk, mmMsg) = await UpdateMattermostPasswordAsync(normalizedUsername, newPassword);
|
||||||
|
results.Add($"Mattermost: {mmMsg}");
|
||||||
|
if (!mmOk) allSuccess = false;
|
||||||
|
|
||||||
|
// 2. Mailcow
|
||||||
|
var (mailOk, mailMsg) = await UpdateMailcowPasswordAsync(normalizedUsername, newPassword);
|
||||||
|
results.Add($"Mailcow: {mailMsg}");
|
||||||
|
if (!mailOk) allSuccess = false;
|
||||||
|
|
||||||
|
// 3. Gitea
|
||||||
|
var (giteaOk, giteaMsg) = await UpdateGiteaPasswordAsync(normalizedUsername, newPassword);
|
||||||
|
results.Add($"Gitea: {giteaMsg}");
|
||||||
|
if (!giteaOk) allSuccess = false;
|
||||||
|
|
||||||
|
var summary = string.Join("; ", results);
|
||||||
|
_logger.LogInformation("Password sync for {Username} (normalized: {NormalizedUsername}): {Summary}", username, normalizedUsername, summary);
|
||||||
|
|
||||||
|
return (allSuccess, summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Application status update ---
|
||||||
|
|
||||||
|
private async Task<(bool Success, string Message)> UpdateApplicationStatusAsync(
|
||||||
|
string ticketId, bool mattermostProvisioned, bool mailcowProvisioned, bool giteaProvisioned = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("SilverDesk");
|
||||||
|
|
||||||
|
var lookupResponse = await client.GetAsync($"/api/developer-program/applications?ticketId={ticketId}");
|
||||||
|
if (!lookupResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var body = await lookupResponse.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Failed to look up application by ticket {TicketId}: {Status} {Body}",
|
||||||
|
ticketId, lookupResponse.StatusCode, body);
|
||||||
|
return (false, $"Lookup failed ({lookupResponse.StatusCode})");
|
||||||
|
}
|
||||||
|
|
||||||
|
var apps = await lookupResponse.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
if (apps.GetArrayLength() == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No application found for ticket {TicketId}", ticketId);
|
||||||
|
return (false, "No application found for ticket");
|
||||||
|
}
|
||||||
|
|
||||||
|
var appId = apps[0].GetProperty("id").GetString();
|
||||||
|
|
||||||
|
var updatePayload = new
|
||||||
|
{
|
||||||
|
status = 1, // ApplicationStatus.Approved
|
||||||
|
mattermostProvisioned,
|
||||||
|
mailcowProvisioned,
|
||||||
|
giteaProvisioned
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateResponse = await client.PutAsJsonAsync($"/api/developer-program/applications/{appId}", updatePayload);
|
||||||
|
if (updateResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Application {AppId} updated to Approved for ticket {TicketId}", appId, ticketId);
|
||||||
|
return (true, "Updated to Approved");
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateBody = await updateResponse.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Failed to update application {AppId}: {Status} {Body}",
|
||||||
|
appId, updateResponse.StatusCode, updateBody);
|
||||||
|
return (false, $"Update failed ({updateResponse.StatusCode})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error updating application status for ticket {TicketId}", ticketId);
|
||||||
|
return (false, $"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Service account creation ---
|
||||||
|
|
||||||
|
public async Task<(bool Success, string Message)> CreateSilverDeskUserAsync(string username, string email, string fullName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("SilverDesk");
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
fullName,
|
||||||
|
password = Guid.NewGuid().ToString("N")[..16] + "!A1"
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(payload);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await client.PostAsync("/api/users", content);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return (true, "User created");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("SilverDESK user creation failed: {Status} {Body}", response.StatusCode, body);
|
||||||
|
return (false, $"Failed ({response.StatusCode})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "SilverDESK user creation error for {Username}", username);
|
||||||
|
return (false, $"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool Success, string Message)> CreateMattermostUserAsync(
|
||||||
|
string username, string email, string fullName, string password)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("Mattermost");
|
||||||
|
var nameParts = fullName.Split(' ', 2);
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
first_name = nameParts[0],
|
||||||
|
last_name = nameParts.Length > 1 ? nameParts[1] : "",
|
||||||
|
password
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(payload);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await client.PostAsync("/api/v4/users", content);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return (true, "User created");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Mattermost user creation failed: {Status} {Body}", response.StatusCode, body);
|
||||||
|
return (false, $"Failed ({response.StatusCode})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Mattermost user creation error for {Username}", username);
|
||||||
|
return (false, $"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool Success, string Message)> AddMattermostUserToTeamAsync(string username)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("Mattermost");
|
||||||
|
|
||||||
|
// Look up user ID by username
|
||||||
|
var userResponse = await client.GetAsync($"/api/v4/users/username/{username}");
|
||||||
|
if (!userResponse.IsSuccessStatusCode)
|
||||||
|
return (false, $"User lookup failed ({userResponse.StatusCode})");
|
||||||
|
|
||||||
|
var userData = await userResponse.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var userId = userData.GetProperty("id").GetString();
|
||||||
|
|
||||||
|
// Add to SilverLABS team
|
||||||
|
var teamId = _configuration["Mattermost:TeamId"] ?? "ear83bc7nprzpe878ey7hxza7h";
|
||||||
|
var payload = new { team_id = teamId, user_id = userId };
|
||||||
|
var response = await client.PostAsJsonAsync($"/api/v4/teams/{teamId}/members", payload);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return (true, "Added to team");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Mattermost team join failed: {Status} {Body}", response.StatusCode, body);
|
||||||
|
return (false, $"Team join failed ({response.StatusCode})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Mattermost team join error for {Username}", username);
|
||||||
|
return (false, $"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool Success, string Message)> CreateMailcowMailboxAsync(
|
||||||
|
string username, string fullName, string password)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("Mailcow");
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
local_part = username,
|
||||||
|
domain = "silverlabs.uk",
|
||||||
|
name = fullName,
|
||||||
|
password,
|
||||||
|
password2 = password,
|
||||||
|
quota = 1024, // 1GB
|
||||||
|
active = 1,
|
||||||
|
force_pw_update = 0
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(payload);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await client.PostAsync("/api/v1/add/mailbox", content);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return (true, "Mailbox created");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Mailcow mailbox creation failed: {Status} {Body}", response.StatusCode, body);
|
||||||
|
return (false, $"Failed ({response.StatusCode})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Mailcow mailbox creation error for {Username}", username);
|
||||||
|
return (false, $"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool Success, string Message)> CreateGiteaUserAsync(
|
||||||
|
string username, string email, string fullName, string password)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("Gitea");
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
full_name = fullName,
|
||||||
|
login_name = username,
|
||||||
|
must_change_password = false,
|
||||||
|
password,
|
||||||
|
send_notify = false,
|
||||||
|
username,
|
||||||
|
visibility = "public"
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(payload);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await client.PostAsync("/api/v1/admin/users", content);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return (true, "User created");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Gitea user creation failed: {Status} {Body}", response.StatusCode, body);
|
||||||
|
return (false, $"Failed ({response.StatusCode})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Gitea user creation error for {Username}", username);
|
||||||
|
return (false, $"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Password update methods ---
|
||||||
|
|
||||||
|
private async Task<(bool Success, string Message)> UpdateMattermostPasswordAsync(string username, string newPassword)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("Mattermost");
|
||||||
|
|
||||||
|
// Look up user ID by username
|
||||||
|
var userResponse = await client.GetAsync($"/api/v4/users/username/{username}");
|
||||||
|
if (!userResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var lookupBody = await userResponse.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Mattermost user lookup failed for {Username}: {Status} {Body}",
|
||||||
|
username, userResponse.StatusCode, lookupBody);
|
||||||
|
return (false, $"User not found ({userResponse.StatusCode})");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = await userResponse.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var userId = userData.GetProperty("id").GetString();
|
||||||
|
|
||||||
|
// Update password (admin reset — no old password needed with bot token)
|
||||||
|
var payload = new { new_password = newPassword };
|
||||||
|
var response = await client.PutAsJsonAsync($"/api/v4/users/{userId}/password", payload);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return (true, "Password updated");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Mattermost password update failed for {Username}: {Status} {Body}", username, response.StatusCode, body);
|
||||||
|
return (false, $"Failed ({response.StatusCode})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Mattermost password update error for {Username}", username);
|
||||||
|
return (false, $"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool Success, string Message)> UpdateMailcowPasswordAsync(string username, string newPassword)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("Mailcow");
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
items = new[] { $"{username}@silverlabs.uk" },
|
||||||
|
attr = new
|
||||||
|
{
|
||||||
|
password = newPassword,
|
||||||
|
password2 = newPassword
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(payload);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await client.PostAsync("/api/v1/edit/mailbox", content);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return (true, "Password updated");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Mailcow password update failed for {Username}: {Status} {Body}", username, response.StatusCode, body);
|
||||||
|
return (false, $"Failed ({response.StatusCode})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Mailcow password update error for {Username}", username);
|
||||||
|
return (false, $"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool Success, string Message)> UpdateGiteaPasswordAsync(string username, string newPassword)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("Gitea");
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
login_name = username,
|
||||||
|
password = newPassword,
|
||||||
|
must_change_password = false,
|
||||||
|
source_id = 0
|
||||||
|
};
|
||||||
|
var json = JsonSerializer.Serialize(payload);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Patch, $"/api/v1/admin/users/{username}")
|
||||||
|
{
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return (true, "Password updated");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Gitea password update failed for {Username}: {Status} {Body}", username, response.StatusCode, body);
|
||||||
|
return (false, $"Failed ({response.StatusCode})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Gitea password update error for {Username}", username);
|
||||||
|
return (false, $"Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
BlazorApp/SilverLabs.Website.csproj
Normal file
9
BlazorApp/SilverLabs.Website.csproj
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
8
BlazorApp/appsettings.Development.json
Normal file
8
BlazorApp/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
BlazorApp/appsettings.json
Normal file
28
BlazorApp/appsettings.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"SilverDesk": {
|
||||||
|
"BaseUrl": "https://silverdesk.silverlabs.uk",
|
||||||
|
"ApiKey": "silverdesk-mcp-2025-secure-key"
|
||||||
|
},
|
||||||
|
"Mattermost": {
|
||||||
|
"BaseUrl": "https://ops.silverlined.uk",
|
||||||
|
"ApiToken": "ktmfkpxz7ffr5g1imuqg8hm58c",
|
||||||
|
"TeamId": "ear83bc7nprzpe878ey7hxza7h"
|
||||||
|
},
|
||||||
|
"Mailcow": {
|
||||||
|
"BaseUrl": "https://mail.silverlined.uk",
|
||||||
|
"ApiKey": "2A21AA-47E4E5-46DD62-A650F0-BC7566"
|
||||||
|
},
|
||||||
|
"Gitea": {
|
||||||
|
"BaseUrl": "https://git.silverlabs.uk",
|
||||||
|
"ApiToken": "70ec152b27ee12d8a2cfb7241df5735351df72cd"
|
||||||
|
},
|
||||||
|
"SiteBaseUrl": "https://silverlabs.uk",
|
||||||
|
"AdminApiKey": "aawb2MHblbfmqdhcS7Xp2/ibQOUbUE1BDoqdJOu0bjM="
|
||||||
|
}
|
||||||
60
BlazorApp/wwwroot/app.css
Normal file
60
BlazorApp/wwwroot/app.css
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
html, body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, .btn-link {
|
||||||
|
color: #006bb7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1b6ec2;
|
||||||
|
border-color: #1861ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||||
|
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-top: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid.modified:not([type=checkbox]) {
|
||||||
|
outline: 1px solid #26b050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
outline: 1px solid #e50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
color: #e50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary {
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||||
|
padding: 1rem 1rem 1rem 3.7rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary::after {
|
||||||
|
content: "An error has occurred."
|
||||||
|
}
|
||||||
|
|
||||||
|
.darker-border-checkbox.form-check-input {
|
||||||
|
border-color: #929292;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
563
BlazorApp/wwwroot/developers-styles.css
Normal file
563
BlazorApp/wwwroot/developers-styles.css
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
/* Developers Page Styles */
|
||||||
|
|
||||||
|
.dev-container {
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-header h1 {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: linear-gradient(135deg, #4DD0E1 0%, #00B8D4 40%, #1E5A9E 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-subtitle {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.dev-section {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
animation: devFadeIn 0.5s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-section:nth-child(2) { animation-delay: 0.1s; }
|
||||||
|
.dev-section:nth-child(3) { animation-delay: 0.15s; }
|
||||||
|
.dev-section:nth-child(4) { animation-delay: 0.2s; }
|
||||||
|
.dev-section:nth-child(5) { animation-delay: 0.25s; }
|
||||||
|
.dev-section:nth-child(6) { animation-delay: 0.3s; }
|
||||||
|
|
||||||
|
@keyframes devFadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(16px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-section-fade-in {
|
||||||
|
animation: devFadeIn 0.4s ease both !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-section-title {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: #4DD0E1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-section-desc {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Role Selector */
|
||||||
|
.role-selector {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-card {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-card:hover {
|
||||||
|
border-color: rgba(77, 208, 225, 0.4);
|
||||||
|
background: rgba(77, 208, 225, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-card.role-active {
|
||||||
|
border-color: #4DD0E1;
|
||||||
|
background: rgba(77, 208, 225, 0.1);
|
||||||
|
box-shadow: 0 0 24px rgba(77, 208, 225, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
margin: 0 auto 0.75rem;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-active .role-icon {
|
||||||
|
background: rgba(77, 208, 225, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-icon svg {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
stroke: rgba(255, 255, 255, 0.6);
|
||||||
|
transition: stroke 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-active .role-icon svg {
|
||||||
|
stroke: #4DD0E1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-card h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-card p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Elements */
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: #4DD0E1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(77, 208, 225, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Username status */
|
||||||
|
.username-status {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-checking {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-available {
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-taken {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-error {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-format-error {
|
||||||
|
color: #fb923c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validation messages */
|
||||||
|
.validation-message {
|
||||||
|
color: #f87171;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Platform Chips */
|
||||||
|
.platform-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-chip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 100px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-chip input[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-chip:hover {
|
||||||
|
border-color: rgba(77, 208, 225, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-chip.platform-active {
|
||||||
|
background: rgba(77, 208, 225, 0.12);
|
||||||
|
border-color: #4DD0E1;
|
||||||
|
color: #4DD0E1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Perks */
|
||||||
|
.dev-perks {
|
||||||
|
background: linear-gradient(135deg, rgba(30, 90, 158, 0.15) 0%, rgba(0, 184, 212, 0.08) 100%);
|
||||||
|
border-color: rgba(77, 208, 225, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.perks-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perk-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perk-item strong {
|
||||||
|
color: #4DD0E1;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perk-item span {
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit Area */
|
||||||
|
.dev-submit-area {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.85rem 2.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
border: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #1E5A9E 0%, #00B8D4 100%);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 184, 212, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-btn-primary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 184, 212, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-btn-secondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-spinner {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.7s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error / Success */
|
||||||
|
.dev-error {
|
||||||
|
background: rgba(248, 113, 113, 0.1);
|
||||||
|
border: 1px solid rgba(248, 113, 113, 0.3);
|
||||||
|
color: #f87171;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-success-panel {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(77, 208, 225, 0.25);
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
animation: devFadeIn 0.5s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
background: rgba(77, 208, 225, 0.15);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-icon svg {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
stroke: #4DD0E1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-success-panel h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: #4DD0E1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-success-panel p {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
max-width: 500px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back Link - reuse from sdk-styles */
|
||||||
|
.dev-container .back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #4DD0E1;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-container .back-link:hover {
|
||||||
|
color: #00B8D4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Star Rating */
|
||||||
|
.star-rating {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rating-star {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(255, 255, 255, 0.2);
|
||||||
|
transition: color 0.15s ease, transform 0.15s ease;
|
||||||
|
user-select: none;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rating-star:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rating-star.star-filled {
|
||||||
|
color: #4DD0E1;
|
||||||
|
text-shadow: 0 0 8px rgba(77, 208, 225, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-labels {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
max-width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Experience Selector */
|
||||||
|
.experience-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-btn {
|
||||||
|
padding: 0.45rem 1.1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 100px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-btn:hover {
|
||||||
|
border-color: rgba(77, 208, 225, 0.4);
|
||||||
|
background: rgba(77, 208, 225, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-btn.exp-active {
|
||||||
|
background: rgba(77, 208, 225, 0.12);
|
||||||
|
border-color: #4DD0E1;
|
||||||
|
color: #4DD0E1;
|
||||||
|
box-shadow: 0 0 12px rgba(77, 208, 225, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skill Bubbles */
|
||||||
|
.skill-category-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-category-label:first-of-type {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-bubbles {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-bubble {
|
||||||
|
padding: 0.35rem 0.85rem;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 100px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-bubble:hover {
|
||||||
|
border-color: rgba(77, 208, 225, 0.35);
|
||||||
|
background: rgba(77, 208, 225, 0.05);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-bubble.skill-active {
|
||||||
|
background: rgba(77, 208, 225, 0.12);
|
||||||
|
border-color: #4DD0E1;
|
||||||
|
color: #4DD0E1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dev-header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-selector {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perks-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-container {
|
||||||
|
padding: 0 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-section {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.dev-header h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
BlazorApp/wwwroot/favicon.png
Normal file
BIN
BlazorApp/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
4085
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
4085
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4084
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
4084
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
597
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
597
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2024 The Bootstrap Authors
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
|
*/
|
||||||
|
:root,
|
||||||
|
[data-bs-theme=light] {
|
||||||
|
--bs-blue: #0d6efd;
|
||||||
|
--bs-indigo: #6610f2;
|
||||||
|
--bs-purple: #6f42c1;
|
||||||
|
--bs-pink: #d63384;
|
||||||
|
--bs-red: #dc3545;
|
||||||
|
--bs-orange: #fd7e14;
|
||||||
|
--bs-yellow: #ffc107;
|
||||||
|
--bs-green: #198754;
|
||||||
|
--bs-teal: #20c997;
|
||||||
|
--bs-cyan: #0dcaf0;
|
||||||
|
--bs-black: #000;
|
||||||
|
--bs-white: #fff;
|
||||||
|
--bs-gray: #6c757d;
|
||||||
|
--bs-gray-dark: #343a40;
|
||||||
|
--bs-gray-100: #f8f9fa;
|
||||||
|
--bs-gray-200: #e9ecef;
|
||||||
|
--bs-gray-300: #dee2e6;
|
||||||
|
--bs-gray-400: #ced4da;
|
||||||
|
--bs-gray-500: #adb5bd;
|
||||||
|
--bs-gray-600: #6c757d;
|
||||||
|
--bs-gray-700: #495057;
|
||||||
|
--bs-gray-800: #343a40;
|
||||||
|
--bs-gray-900: #212529;
|
||||||
|
--bs-primary: #0d6efd;
|
||||||
|
--bs-secondary: #6c757d;
|
||||||
|
--bs-success: #198754;
|
||||||
|
--bs-info: #0dcaf0;
|
||||||
|
--bs-warning: #ffc107;
|
||||||
|
--bs-danger: #dc3545;
|
||||||
|
--bs-light: #f8f9fa;
|
||||||
|
--bs-dark: #212529;
|
||||||
|
--bs-primary-rgb: 13, 110, 253;
|
||||||
|
--bs-secondary-rgb: 108, 117, 125;
|
||||||
|
--bs-success-rgb: 25, 135, 84;
|
||||||
|
--bs-info-rgb: 13, 202, 240;
|
||||||
|
--bs-warning-rgb: 255, 193, 7;
|
||||||
|
--bs-danger-rgb: 220, 53, 69;
|
||||||
|
--bs-light-rgb: 248, 249, 250;
|
||||||
|
--bs-dark-rgb: 33, 37, 41;
|
||||||
|
--bs-primary-text-emphasis: #052c65;
|
||||||
|
--bs-secondary-text-emphasis: #2b2f32;
|
||||||
|
--bs-success-text-emphasis: #0a3622;
|
||||||
|
--bs-info-text-emphasis: #055160;
|
||||||
|
--bs-warning-text-emphasis: #664d03;
|
||||||
|
--bs-danger-text-emphasis: #58151c;
|
||||||
|
--bs-light-text-emphasis: #495057;
|
||||||
|
--bs-dark-text-emphasis: #495057;
|
||||||
|
--bs-primary-bg-subtle: #cfe2ff;
|
||||||
|
--bs-secondary-bg-subtle: #e2e3e5;
|
||||||
|
--bs-success-bg-subtle: #d1e7dd;
|
||||||
|
--bs-info-bg-subtle: #cff4fc;
|
||||||
|
--bs-warning-bg-subtle: #fff3cd;
|
||||||
|
--bs-danger-bg-subtle: #f8d7da;
|
||||||
|
--bs-light-bg-subtle: #fcfcfd;
|
||||||
|
--bs-dark-bg-subtle: #ced4da;
|
||||||
|
--bs-primary-border-subtle: #9ec5fe;
|
||||||
|
--bs-secondary-border-subtle: #c4c8cb;
|
||||||
|
--bs-success-border-subtle: #a3cfbb;
|
||||||
|
--bs-info-border-subtle: #9eeaf9;
|
||||||
|
--bs-warning-border-subtle: #ffe69c;
|
||||||
|
--bs-danger-border-subtle: #f1aeb5;
|
||||||
|
--bs-light-border-subtle: #e9ecef;
|
||||||
|
--bs-dark-border-subtle: #adb5bd;
|
||||||
|
--bs-white-rgb: 255, 255, 255;
|
||||||
|
--bs-black-rgb: 0, 0, 0;
|
||||||
|
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||||
|
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||||
|
--bs-body-font-size: 1rem;
|
||||||
|
--bs-body-font-weight: 400;
|
||||||
|
--bs-body-line-height: 1.5;
|
||||||
|
--bs-body-color: #212529;
|
||||||
|
--bs-body-color-rgb: 33, 37, 41;
|
||||||
|
--bs-body-bg: #fff;
|
||||||
|
--bs-body-bg-rgb: 255, 255, 255;
|
||||||
|
--bs-emphasis-color: #000;
|
||||||
|
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||||
|
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||||
|
--bs-secondary-color-rgb: 33, 37, 41;
|
||||||
|
--bs-secondary-bg: #e9ecef;
|
||||||
|
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||||
|
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||||
|
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||||
|
--bs-tertiary-bg: #f8f9fa;
|
||||||
|
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||||
|
--bs-heading-color: inherit;
|
||||||
|
--bs-link-color: #0d6efd;
|
||||||
|
--bs-link-color-rgb: 13, 110, 253;
|
||||||
|
--bs-link-decoration: underline;
|
||||||
|
--bs-link-hover-color: #0a58ca;
|
||||||
|
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||||
|
--bs-code-color: #d63384;
|
||||||
|
--bs-highlight-color: #212529;
|
||||||
|
--bs-highlight-bg: #fff3cd;
|
||||||
|
--bs-border-width: 1px;
|
||||||
|
--bs-border-style: solid;
|
||||||
|
--bs-border-color: #dee2e6;
|
||||||
|
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||||
|
--bs-border-radius: 0.375rem;
|
||||||
|
--bs-border-radius-sm: 0.25rem;
|
||||||
|
--bs-border-radius-lg: 0.5rem;
|
||||||
|
--bs-border-radius-xl: 1rem;
|
||||||
|
--bs-border-radius-xxl: 2rem;
|
||||||
|
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||||
|
--bs-border-radius-pill: 50rem;
|
||||||
|
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||||
|
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||||
|
--bs-focus-ring-width: 0.25rem;
|
||||||
|
--bs-focus-ring-opacity: 0.25;
|
||||||
|
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||||
|
--bs-form-valid-color: #198754;
|
||||||
|
--bs-form-valid-border-color: #198754;
|
||||||
|
--bs-form-invalid-color: #dc3545;
|
||||||
|
--bs-form-invalid-border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme=dark] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bs-body-color: #dee2e6;
|
||||||
|
--bs-body-color-rgb: 222, 226, 230;
|
||||||
|
--bs-body-bg: #212529;
|
||||||
|
--bs-body-bg-rgb: 33, 37, 41;
|
||||||
|
--bs-emphasis-color: #fff;
|
||||||
|
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||||
|
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||||
|
--bs-secondary-color-rgb: 222, 226, 230;
|
||||||
|
--bs-secondary-bg: #343a40;
|
||||||
|
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||||
|
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||||
|
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||||
|
--bs-tertiary-bg: #2b3035;
|
||||||
|
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||||
|
--bs-primary-text-emphasis: #6ea8fe;
|
||||||
|
--bs-secondary-text-emphasis: #a7acb1;
|
||||||
|
--bs-success-text-emphasis: #75b798;
|
||||||
|
--bs-info-text-emphasis: #6edff6;
|
||||||
|
--bs-warning-text-emphasis: #ffda6a;
|
||||||
|
--bs-danger-text-emphasis: #ea868f;
|
||||||
|
--bs-light-text-emphasis: #f8f9fa;
|
||||||
|
--bs-dark-text-emphasis: #dee2e6;
|
||||||
|
--bs-primary-bg-subtle: #031633;
|
||||||
|
--bs-secondary-bg-subtle: #161719;
|
||||||
|
--bs-success-bg-subtle: #051b11;
|
||||||
|
--bs-info-bg-subtle: #032830;
|
||||||
|
--bs-warning-bg-subtle: #332701;
|
||||||
|
--bs-danger-bg-subtle: #2c0b0e;
|
||||||
|
--bs-light-bg-subtle: #343a40;
|
||||||
|
--bs-dark-bg-subtle: #1a1d20;
|
||||||
|
--bs-primary-border-subtle: #084298;
|
||||||
|
--bs-secondary-border-subtle: #41464b;
|
||||||
|
--bs-success-border-subtle: #0f5132;
|
||||||
|
--bs-info-border-subtle: #087990;
|
||||||
|
--bs-warning-border-subtle: #997404;
|
||||||
|
--bs-danger-border-subtle: #842029;
|
||||||
|
--bs-light-border-subtle: #495057;
|
||||||
|
--bs-dark-border-subtle: #343a40;
|
||||||
|
--bs-heading-color: inherit;
|
||||||
|
--bs-link-color: #6ea8fe;
|
||||||
|
--bs-link-hover-color: #8bb9fe;
|
||||||
|
--bs-link-color-rgb: 110, 168, 254;
|
||||||
|
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||||
|
--bs-code-color: #e685b5;
|
||||||
|
--bs-highlight-color: #dee2e6;
|
||||||
|
--bs-highlight-bg: #664d03;
|
||||||
|
--bs-border-color: #495057;
|
||||||
|
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||||
|
--bs-form-valid-color: #75b798;
|
||||||
|
--bs-form-valid-border-color: #75b798;
|
||||||
|
--bs-form-invalid-color: #ea868f;
|
||||||
|
--bs-form-invalid-border-color: #ea868f;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
:root {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--bs-body-font-family);
|
||||||
|
font-size: var(--bs-body-font-size);
|
||||||
|
font-weight: var(--bs-body-font-weight);
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
text-align: var(--bs-body-text-align);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: inherit;
|
||||||
|
border: 0;
|
||||||
|
border-top: var(--bs-border-width) solid;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--bs-heading-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: calc(1.375rem + 1.5vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: calc(1.325rem + 0.9vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: calc(1.3rem + 0.6vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h3 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
cursor: help;
|
||||||
|
-webkit-text-decoration-skip-ink: none;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
address {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol ol,
|
||||||
|
ul ul,
|
||||||
|
ol ul,
|
||||||
|
ul ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
padding: 0.1875em;
|
||||||
|
color: var(--bs-highlight-color);
|
||||||
|
background-color: var(--bs-highlight-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.75em;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: var(--bs-font-monospace);
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: var(--bs-code-color);
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
a > code {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
padding: 0.1875rem 0.375rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: var(--bs-body-bg);
|
||||||
|
background-color: var(--bs-body-color);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
kbd kbd {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
caption-side: bottom;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus:not(:focus-visible) {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
optgroup,
|
||||||
|
textarea {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role=button] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
select:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type=button],
|
||||||
|
[type=reset],
|
||||||
|
[type=submit] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
button:not(:disabled),
|
||||||
|
[type=button]:not(:disabled),
|
||||||
|
[type=reset]:not(:disabled),
|
||||||
|
[type=submit]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
legend {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legend + * {
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper,
|
||||||
|
::-webkit-datetime-edit-text,
|
||||||
|
::-webkit-datetime-edit-minute,
|
||||||
|
::-webkit-datetime-edit-hour-field,
|
||||||
|
::-webkit-datetime-edit-day-field,
|
||||||
|
::-webkit-datetime-edit-month-field,
|
||||||
|
::-webkit-datetime-edit-year-field {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=search] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rtl:raw:
|
||||||
|
[type="tel"],
|
||||||
|
[type="url"],
|
||||||
|
[type="email"],
|
||||||
|
[type="number"] {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
::file-selector-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||||
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
594
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
594
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
@@ -0,0 +1,594 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2024 The Bootstrap Authors
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
|
*/
|
||||||
|
:root,
|
||||||
|
[data-bs-theme=light] {
|
||||||
|
--bs-blue: #0d6efd;
|
||||||
|
--bs-indigo: #6610f2;
|
||||||
|
--bs-purple: #6f42c1;
|
||||||
|
--bs-pink: #d63384;
|
||||||
|
--bs-red: #dc3545;
|
||||||
|
--bs-orange: #fd7e14;
|
||||||
|
--bs-yellow: #ffc107;
|
||||||
|
--bs-green: #198754;
|
||||||
|
--bs-teal: #20c997;
|
||||||
|
--bs-cyan: #0dcaf0;
|
||||||
|
--bs-black: #000;
|
||||||
|
--bs-white: #fff;
|
||||||
|
--bs-gray: #6c757d;
|
||||||
|
--bs-gray-dark: #343a40;
|
||||||
|
--bs-gray-100: #f8f9fa;
|
||||||
|
--bs-gray-200: #e9ecef;
|
||||||
|
--bs-gray-300: #dee2e6;
|
||||||
|
--bs-gray-400: #ced4da;
|
||||||
|
--bs-gray-500: #adb5bd;
|
||||||
|
--bs-gray-600: #6c757d;
|
||||||
|
--bs-gray-700: #495057;
|
||||||
|
--bs-gray-800: #343a40;
|
||||||
|
--bs-gray-900: #212529;
|
||||||
|
--bs-primary: #0d6efd;
|
||||||
|
--bs-secondary: #6c757d;
|
||||||
|
--bs-success: #198754;
|
||||||
|
--bs-info: #0dcaf0;
|
||||||
|
--bs-warning: #ffc107;
|
||||||
|
--bs-danger: #dc3545;
|
||||||
|
--bs-light: #f8f9fa;
|
||||||
|
--bs-dark: #212529;
|
||||||
|
--bs-primary-rgb: 13, 110, 253;
|
||||||
|
--bs-secondary-rgb: 108, 117, 125;
|
||||||
|
--bs-success-rgb: 25, 135, 84;
|
||||||
|
--bs-info-rgb: 13, 202, 240;
|
||||||
|
--bs-warning-rgb: 255, 193, 7;
|
||||||
|
--bs-danger-rgb: 220, 53, 69;
|
||||||
|
--bs-light-rgb: 248, 249, 250;
|
||||||
|
--bs-dark-rgb: 33, 37, 41;
|
||||||
|
--bs-primary-text-emphasis: #052c65;
|
||||||
|
--bs-secondary-text-emphasis: #2b2f32;
|
||||||
|
--bs-success-text-emphasis: #0a3622;
|
||||||
|
--bs-info-text-emphasis: #055160;
|
||||||
|
--bs-warning-text-emphasis: #664d03;
|
||||||
|
--bs-danger-text-emphasis: #58151c;
|
||||||
|
--bs-light-text-emphasis: #495057;
|
||||||
|
--bs-dark-text-emphasis: #495057;
|
||||||
|
--bs-primary-bg-subtle: #cfe2ff;
|
||||||
|
--bs-secondary-bg-subtle: #e2e3e5;
|
||||||
|
--bs-success-bg-subtle: #d1e7dd;
|
||||||
|
--bs-info-bg-subtle: #cff4fc;
|
||||||
|
--bs-warning-bg-subtle: #fff3cd;
|
||||||
|
--bs-danger-bg-subtle: #f8d7da;
|
||||||
|
--bs-light-bg-subtle: #fcfcfd;
|
||||||
|
--bs-dark-bg-subtle: #ced4da;
|
||||||
|
--bs-primary-border-subtle: #9ec5fe;
|
||||||
|
--bs-secondary-border-subtle: #c4c8cb;
|
||||||
|
--bs-success-border-subtle: #a3cfbb;
|
||||||
|
--bs-info-border-subtle: #9eeaf9;
|
||||||
|
--bs-warning-border-subtle: #ffe69c;
|
||||||
|
--bs-danger-border-subtle: #f1aeb5;
|
||||||
|
--bs-light-border-subtle: #e9ecef;
|
||||||
|
--bs-dark-border-subtle: #adb5bd;
|
||||||
|
--bs-white-rgb: 255, 255, 255;
|
||||||
|
--bs-black-rgb: 0, 0, 0;
|
||||||
|
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||||
|
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||||
|
--bs-body-font-size: 1rem;
|
||||||
|
--bs-body-font-weight: 400;
|
||||||
|
--bs-body-line-height: 1.5;
|
||||||
|
--bs-body-color: #212529;
|
||||||
|
--bs-body-color-rgb: 33, 37, 41;
|
||||||
|
--bs-body-bg: #fff;
|
||||||
|
--bs-body-bg-rgb: 255, 255, 255;
|
||||||
|
--bs-emphasis-color: #000;
|
||||||
|
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||||
|
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||||
|
--bs-secondary-color-rgb: 33, 37, 41;
|
||||||
|
--bs-secondary-bg: #e9ecef;
|
||||||
|
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||||
|
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||||
|
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||||
|
--bs-tertiary-bg: #f8f9fa;
|
||||||
|
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||||
|
--bs-heading-color: inherit;
|
||||||
|
--bs-link-color: #0d6efd;
|
||||||
|
--bs-link-color-rgb: 13, 110, 253;
|
||||||
|
--bs-link-decoration: underline;
|
||||||
|
--bs-link-hover-color: #0a58ca;
|
||||||
|
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||||
|
--bs-code-color: #d63384;
|
||||||
|
--bs-highlight-color: #212529;
|
||||||
|
--bs-highlight-bg: #fff3cd;
|
||||||
|
--bs-border-width: 1px;
|
||||||
|
--bs-border-style: solid;
|
||||||
|
--bs-border-color: #dee2e6;
|
||||||
|
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||||
|
--bs-border-radius: 0.375rem;
|
||||||
|
--bs-border-radius-sm: 0.25rem;
|
||||||
|
--bs-border-radius-lg: 0.5rem;
|
||||||
|
--bs-border-radius-xl: 1rem;
|
||||||
|
--bs-border-radius-xxl: 2rem;
|
||||||
|
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||||
|
--bs-border-radius-pill: 50rem;
|
||||||
|
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||||
|
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||||
|
--bs-focus-ring-width: 0.25rem;
|
||||||
|
--bs-focus-ring-opacity: 0.25;
|
||||||
|
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||||
|
--bs-form-valid-color: #198754;
|
||||||
|
--bs-form-valid-border-color: #198754;
|
||||||
|
--bs-form-invalid-color: #dc3545;
|
||||||
|
--bs-form-invalid-border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme=dark] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bs-body-color: #dee2e6;
|
||||||
|
--bs-body-color-rgb: 222, 226, 230;
|
||||||
|
--bs-body-bg: #212529;
|
||||||
|
--bs-body-bg-rgb: 33, 37, 41;
|
||||||
|
--bs-emphasis-color: #fff;
|
||||||
|
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||||
|
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||||
|
--bs-secondary-color-rgb: 222, 226, 230;
|
||||||
|
--bs-secondary-bg: #343a40;
|
||||||
|
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||||
|
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||||
|
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||||
|
--bs-tertiary-bg: #2b3035;
|
||||||
|
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||||
|
--bs-primary-text-emphasis: #6ea8fe;
|
||||||
|
--bs-secondary-text-emphasis: #a7acb1;
|
||||||
|
--bs-success-text-emphasis: #75b798;
|
||||||
|
--bs-info-text-emphasis: #6edff6;
|
||||||
|
--bs-warning-text-emphasis: #ffda6a;
|
||||||
|
--bs-danger-text-emphasis: #ea868f;
|
||||||
|
--bs-light-text-emphasis: #f8f9fa;
|
||||||
|
--bs-dark-text-emphasis: #dee2e6;
|
||||||
|
--bs-primary-bg-subtle: #031633;
|
||||||
|
--bs-secondary-bg-subtle: #161719;
|
||||||
|
--bs-success-bg-subtle: #051b11;
|
||||||
|
--bs-info-bg-subtle: #032830;
|
||||||
|
--bs-warning-bg-subtle: #332701;
|
||||||
|
--bs-danger-bg-subtle: #2c0b0e;
|
||||||
|
--bs-light-bg-subtle: #343a40;
|
||||||
|
--bs-dark-bg-subtle: #1a1d20;
|
||||||
|
--bs-primary-border-subtle: #084298;
|
||||||
|
--bs-secondary-border-subtle: #41464b;
|
||||||
|
--bs-success-border-subtle: #0f5132;
|
||||||
|
--bs-info-border-subtle: #087990;
|
||||||
|
--bs-warning-border-subtle: #997404;
|
||||||
|
--bs-danger-border-subtle: #842029;
|
||||||
|
--bs-light-border-subtle: #495057;
|
||||||
|
--bs-dark-border-subtle: #343a40;
|
||||||
|
--bs-heading-color: inherit;
|
||||||
|
--bs-link-color: #6ea8fe;
|
||||||
|
--bs-link-hover-color: #8bb9fe;
|
||||||
|
--bs-link-color-rgb: 110, 168, 254;
|
||||||
|
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||||
|
--bs-code-color: #e685b5;
|
||||||
|
--bs-highlight-color: #dee2e6;
|
||||||
|
--bs-highlight-bg: #664d03;
|
||||||
|
--bs-border-color: #495057;
|
||||||
|
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||||
|
--bs-form-valid-color: #75b798;
|
||||||
|
--bs-form-valid-border-color: #75b798;
|
||||||
|
--bs-form-invalid-color: #ea868f;
|
||||||
|
--bs-form-invalid-border-color: #ea868f;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
:root {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--bs-body-font-family);
|
||||||
|
font-size: var(--bs-body-font-size);
|
||||||
|
font-weight: var(--bs-body-font-weight);
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
text-align: var(--bs-body-text-align);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: inherit;
|
||||||
|
border: 0;
|
||||||
|
border-top: var(--bs-border-width) solid;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--bs-heading-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: calc(1.375rem + 1.5vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: calc(1.325rem + 0.9vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: calc(1.3rem + 0.6vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h3 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
cursor: help;
|
||||||
|
-webkit-text-decoration-skip-ink: none;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
address {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol ol,
|
||||||
|
ul ul,
|
||||||
|
ol ul,
|
||||||
|
ul ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
padding: 0.1875em;
|
||||||
|
color: var(--bs-highlight-color);
|
||||||
|
background-color: var(--bs-highlight-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.75em;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: var(--bs-font-monospace);
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: var(--bs-code-color);
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
a > code {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
padding: 0.1875rem 0.375rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: var(--bs-body-bg);
|
||||||
|
background-color: var(--bs-body-color);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
kbd kbd {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
caption-side: bottom;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus:not(:focus-visible) {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
optgroup,
|
||||||
|
textarea {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role=button] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
select:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type=button],
|
||||||
|
[type=reset],
|
||||||
|
[type=submit] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
button:not(:disabled),
|
||||||
|
[type=button]:not(:disabled),
|
||||||
|
[type=reset]:not(:disabled),
|
||||||
|
[type=submit]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
float: right;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
legend {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legend + * {
|
||||||
|
clear: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper,
|
||||||
|
::-webkit-datetime-edit-text,
|
||||||
|
::-webkit-datetime-edit-minute,
|
||||||
|
::-webkit-datetime-edit-hour-field,
|
||||||
|
::-webkit-datetime-edit-day-field,
|
||||||
|
::-webkit-datetime-edit-month-field,
|
||||||
|
::-webkit-datetime-edit-year-field {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=search] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="tel"],
|
||||||
|
[type="url"],
|
||||||
|
[type="email"],
|
||||||
|
[type="number"] {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
::file-selector-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||||
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5402
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
5402
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5393
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
5393
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
12057
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
12057
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
12030
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
12030
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
6
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6314
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
6314
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
7
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4447
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
vendored
Normal file
4447
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
vendored
Normal file
7
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4494
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js
vendored
Normal file
4494
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
vendored
Normal file
7
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
vendored
Normal file
1
BlazorApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
170
BlazorApp/wwwroot/sdk-styles.css
Normal file
170
BlazorApp/wwwroot/sdk-styles.css
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
.sdk-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdk-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdk-header h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdk-section {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdk-section h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdk-section h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block code {
|
||||||
|
color: #a5f3fc;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-card {
|
||||||
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: rgba(102, 126, 234, 0.6);
|
||||||
|
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-card h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-card p {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-list {
|
||||||
|
counter-reset: step-counter;
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-list li {
|
||||||
|
counter-increment: step-counter;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 3rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-list li::before {
|
||||||
|
content: counter(step-counter);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 2rem;
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
color: #764ba2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdk-container ul {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdk-container p {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdk-container ol {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
35
Dockerfile
35
Dockerfile
@@ -1,19 +1,30 @@
|
|||||||
FROM nginx:alpine
|
# Build stage
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
# Copy website files to nginx html directory
|
# Copy project file and restore dependencies
|
||||||
COPY index.html /usr/share/nginx/html/
|
COPY BlazorApp/SilverLabs.Website.csproj BlazorApp/
|
||||||
COPY styles.css /usr/share/nginx/html/
|
RUN dotnet restore "BlazorApp/SilverLabs.Website.csproj"
|
||||||
COPY script.js /usr/share/nginx/html/
|
|
||||||
COPY logo.png /usr/share/nginx/html/
|
|
||||||
|
|
||||||
# Copy SDK directory with templates
|
# Copy all source files
|
||||||
COPY sdk/ /usr/share/nginx/html/sdk/
|
COPY BlazorApp/ BlazorApp/
|
||||||
|
|
||||||
# Copy custom nginx configuration
|
# Build and publish the application
|
||||||
COPY nginx-site.conf /etc/nginx/conf.d/default.conf
|
WORKDIR /src/BlazorApp
|
||||||
|
RUN dotnet publish "SilverLabs.Website.csproj" -c Release -o /app/publish
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
|
||||||
# Expose port 80
|
# Expose port 80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
# Start nginx
|
# Set environment to production
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ENV ASPNETCORE_URLS=http://+:80
|
||||||
|
|
||||||
|
# Start the Blazor application
|
||||||
|
ENTRYPOINT ["dotnet", "SilverLabs.Website.dll"]
|
||||||
|
|||||||
73
index.html
73
index.html
@@ -1,73 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>SilverLabs - Innovation Gateway</title>
|
|
||||||
<link rel="stylesheet" href="styles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Loading Screen -->
|
|
||||||
<div id="loading-screen" class="loading-screen">
|
|
||||||
<div class="loading-content">
|
|
||||||
<img src="logo.png" alt="SilverLabs Logo" class="loading-logo">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div id="main-content" class="main-content">
|
|
||||||
<header class="header">
|
|
||||||
<img src="logo.png" alt="SilverLabs Logo" class="logo">
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="main">
|
|
||||||
<h1 class="title">Welcome to SilverLabs</h1>
|
|
||||||
<p class="subtitle">Your Innovation Gateway</p>
|
|
||||||
|
|
||||||
<div class="gateway-grid">
|
|
||||||
<a href="https://helpdesk.silverlabs.uk" class="gateway-card">
|
|
||||||
<div class="card-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 class="card-title">Help Desk</h2>
|
|
||||||
<p class="card-description">Support & Assistance</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://appstore.silverlabs.uk" class="gateway-card">
|
|
||||||
<div class="card-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<rect x="3" y="3" width="7" height="7"></rect>
|
|
||||||
<rect x="14" y="3" width="7" height="7"></rect>
|
|
||||||
<rect x="14" y="14" width="7" height="7"></rect>
|
|
||||||
<rect x="3" y="14" width="7" height="7"></rect>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 class="card-title">App Store</h2>
|
|
||||||
<p class="card-description">Applications & Tools</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://cloud.silverlabs.uk" class="gateway-card">
|
|
||||||
<div class="card-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 class="card-title">Cloud</h2>
|
|
||||||
<p class="card-description">Storage & Collaboration</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
<p>© 2025 SilverLabs. All rights reserved.</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="script.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
41
nginx-blazor.conf
Normal file
41
nginx-blazor.conf
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Enable gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript application/json image/jpeg image/png application/wasm;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|wasm|json)$ {
|
||||||
|
expires 1h;
|
||||||
|
add_header Cache-Control "public, max-age=3600";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Blazor framework files - longer cache
|
||||||
|
location /_framework/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# MIME types for Blazor WebAssembly
|
||||||
|
types {
|
||||||
|
application/wasm wasm;
|
||||||
|
application/octet-stream dll;
|
||||||
|
application/json json;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main location - support client-side routing
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Enable gzip compression
|
|
||||||
gzip on;
|
|
||||||
gzip_types text/css application/javascript image/jpeg image/png;
|
|
||||||
gzip_min_length 1000;
|
|
||||||
|
|
||||||
# Cache static assets
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main location
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/index.html /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
}
|
|
||||||
139
script.js
139
script.js
@@ -1,139 +0,0 @@
|
|||||||
// Loading screen functionality
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const loadingScreen = document.getElementById('loading-screen');
|
|
||||||
const mainContent = document.getElementById('main-content');
|
|
||||||
|
|
||||||
// Minimum loading time (in milliseconds) to show the loading screen
|
|
||||||
const minLoadingTime = 2000;
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
// Function to hide loading screen and show main content
|
|
||||||
function hideLoadingScreen() {
|
|
||||||
const elapsedTime = Date.now() - startTime;
|
|
||||||
const remainingTime = Math.max(0, minLoadingTime - elapsedTime);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
loadingScreen.classList.add('fade-out');
|
|
||||||
mainContent.classList.add('visible');
|
|
||||||
|
|
||||||
// Remove loading screen from DOM after transition
|
|
||||||
setTimeout(() => {
|
|
||||||
loadingScreen.style.display = 'none';
|
|
||||||
}, 500);
|
|
||||||
}, remainingTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide loading screen when page is fully loaded
|
|
||||||
if (document.readyState === 'complete') {
|
|
||||||
hideLoadingScreen();
|
|
||||||
} else {
|
|
||||||
window.addEventListener('load', hideLoadingScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add floating particles animation to background
|
|
||||||
createFloatingParticles();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create floating particles for background effect
|
|
||||||
function createFloatingParticles() {
|
|
||||||
const mainContent = document.getElementById('main-content');
|
|
||||||
const particleCount = 20;
|
|
||||||
|
|
||||||
for (let i = 0; i < particleCount; i++) {
|
|
||||||
createParticle(mainContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createParticle(container) {
|
|
||||||
const particle = document.createElement('div');
|
|
||||||
particle.className = 'particle';
|
|
||||||
|
|
||||||
// Random size between 2px and 6px
|
|
||||||
const size = Math.random() * 4 + 2;
|
|
||||||
particle.style.width = `${size}px`;
|
|
||||||
particle.style.height = `${size}px`;
|
|
||||||
|
|
||||||
// Random position
|
|
||||||
particle.style.left = `${Math.random() * 100}%`;
|
|
||||||
particle.style.top = `${Math.random() * 100}%`;
|
|
||||||
|
|
||||||
// Random animation duration between 10s and 30s
|
|
||||||
const duration = Math.random() * 20 + 10;
|
|
||||||
particle.style.animationDuration = `${duration}s`;
|
|
||||||
|
|
||||||
// Random delay
|
|
||||||
const delay = Math.random() * 5;
|
|
||||||
particle.style.animationDelay = `${delay}s`;
|
|
||||||
|
|
||||||
// Random opacity
|
|
||||||
const opacity = Math.random() * 0.3 + 0.1;
|
|
||||||
particle.style.opacity = opacity;
|
|
||||||
|
|
||||||
// Apply styles
|
|
||||||
particle.style.position = 'absolute';
|
|
||||||
particle.style.borderRadius = '50%';
|
|
||||||
particle.style.background = 'rgba(255, 255, 255, 0.5)';
|
|
||||||
particle.style.pointerEvents = 'none';
|
|
||||||
particle.style.zIndex = '0';
|
|
||||||
particle.style.animation = 'float ' + particle.style.animationDuration + ' ease-in-out infinite';
|
|
||||||
|
|
||||||
container.appendChild(particle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add CSS animation for particles
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translate(0, 0) rotate(0deg);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translate(10px, -20px) rotate(90deg);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(-15px, -10px) rotate(180deg);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translate(-10px, 20px) rotate(270deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.particle {
|
|
||||||
filter: blur(1px);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
|
|
||||||
// Add smooth scroll behavior for potential internal links
|
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
||||||
anchor.addEventListener('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const target = document.querySelector(this.getAttribute('href'));
|
|
||||||
if (target) {
|
|
||||||
target.scrollIntoView({
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add interactive hover effect to cards
|
|
||||||
document.querySelectorAll('.gateway-card').forEach(card => {
|
|
||||||
card.addEventListener('mousemove', function(e) {
|
|
||||||
const rect = card.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
|
|
||||||
const centerX = rect.width / 2;
|
|
||||||
const centerY = rect.height / 2;
|
|
||||||
|
|
||||||
const rotateX = (y - centerY) / 10;
|
|
||||||
const rotateY = (centerX - x) / 10;
|
|
||||||
|
|
||||||
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateY(-10px)`;
|
|
||||||
});
|
|
||||||
|
|
||||||
card.addEventListener('mouseleave', function() {
|
|
||||||
card.style.transform = '';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Binary file not shown.
Binary file not shown.
429
sdk/index.html
429
sdk/index.html
@@ -1,429 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>SilverSHELL SDK - SilverLabs</title>
|
|
||||||
<link rel="stylesheet" href="../styles.css">
|
|
||||||
<style>
|
|
||||||
.sdk-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sdk-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sdk-header h1 {
|
|
||||||
font-size: 3rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sdk-section {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding: 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sdk-section h2 {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sdk-section h3 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #a78bfa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block {
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
overflow-x: auto;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block code {
|
|
||||||
color: #a5f3fc;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-card {
|
|
||||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
|
||||||
border-radius: 1rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
text-decoration: none;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
border-color: rgba(102, 126, 234, 0.6);
|
|
||||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-card h3 {
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-card p {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn {
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps-list {
|
|
||||||
counter-reset: step-counter;
|
|
||||||
list-style: none;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps-list li {
|
|
||||||
counter-increment: step-counter;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 3rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps-list li::before {
|
|
||||||
content: counter(step-counter);
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-box {
|
|
||||||
background: rgba(102, 126, 234, 0.1);
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link {
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 2rem;
|
|
||||||
color: #667eea;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link:hover {
|
|
||||||
color: #764ba2;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="main-content" class="main-content">
|
|
||||||
<header class="header">
|
|
||||||
<img src="../logo.png" alt="SilverLabs Logo" class="logo">
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="sdk-container">
|
|
||||||
<div class="sdk-header">
|
|
||||||
<h1>SilverSHELL SDK</h1>
|
|
||||||
<p style="font-size: 1.2rem; color: rgba(255, 255, 255, 0.7);">Build modular Blazor WebAssembly applications with ease</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick Start Section -->
|
|
||||||
<div class="sdk-section">
|
|
||||||
<h2>🚀 Quick Start</h2>
|
|
||||||
<p>Get started with SilverSHELL in just a few minutes. Follow these simple steps:</p>
|
|
||||||
|
|
||||||
<ol class="steps-list">
|
|
||||||
<li>
|
|
||||||
<strong>Download the templates</strong> (see downloads below)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Extract the templates</strong> to a directory of your choice
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Install the templates</strong>
|
|
||||||
<div class="code-block"><code>dotnet new install ./path/to/SilverSHELL.Starter.Template
|
|
||||||
dotnet new install ./path/to/SilverSHELL.AppModule.Template</code></div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Create your first project</strong>
|
|
||||||
<div class="code-block"><code>dotnet new silvershell-starter -n MyApp --pwa true --module-repository true
|
|
||||||
cd MyApp
|
|
||||||
dotnet run</code></div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Access your application</strong> at <code>https://localhost:5001</code>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Downloads Section -->
|
|
||||||
<div class="sdk-section">
|
|
||||||
<h2>📦 Downloads</h2>
|
|
||||||
<p>Download the SilverSHELL project templates to get started:</p>
|
|
||||||
|
|
||||||
<div class="download-grid">
|
|
||||||
<div class="download-card">
|
|
||||||
<h3>Starter Template</h3>
|
|
||||||
<p>Create a new SilverSHELL application with minimal configuration</p>
|
|
||||||
<p><strong>Size:</strong> 6.3 KB</p>
|
|
||||||
<p><strong>Includes:</strong> Blazor WebAssembly setup, module loading, PWA support</p>
|
|
||||||
<a href="downloads/silvershell-starter-template.tar.gz" class="download-btn" download>
|
|
||||||
Download Starter Template
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="download-card">
|
|
||||||
<h3>Module Template</h3>
|
|
||||||
<p>Create reusable SilverSHELL modules with best practices</p>
|
|
||||||
<p><strong>Size:</strong> 14 KB</p>
|
|
||||||
<p><strong>Includes:</strong> Module structure, configuration, CI/CD templates</p>
|
|
||||||
<a href="downloads/silvershell-module-template.tar.gz" class="download-btn" download>
|
|
||||||
Download Module Template
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creating Applications Section -->
|
|
||||||
<div class="sdk-section">
|
|
||||||
<h2>🏗️ Creating Applications</h2>
|
|
||||||
|
|
||||||
<h3>Using the Starter Template</h3>
|
|
||||||
<div class="code-block"><code># Create a new application
|
|
||||||
dotnet new silvershell-starter -n MyAwesomeApp
|
|
||||||
|
|
||||||
# With PWA support
|
|
||||||
dotnet new silvershell-starter -n MyAwesomeApp --pwa true
|
|
||||||
|
|
||||||
# With Module Repository integration
|
|
||||||
dotnet new silvershell-starter -n MyAwesomeApp --module-repository true</code></div>
|
|
||||||
|
|
||||||
<h3>Adding Modules</h3>
|
|
||||||
<p>SilverSHELL supports multiple ways to add modules to your application:</p>
|
|
||||||
|
|
||||||
<h3>Option 1: Configuration File</h3>
|
|
||||||
<p>Edit <code>wwwroot/appsettings.json</code>:</p>
|
|
||||||
<div class="code-block"><code>{
|
|
||||||
"AMS": {
|
|
||||||
"Deployment": {
|
|
||||||
"PreloadModules": [
|
|
||||||
"SilverLabs.SilverSHELL.Auth.Login",
|
|
||||||
"SilverSHELL.Modules.ModuleBrowser"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}</code></div>
|
|
||||||
|
|
||||||
<h3>Option 2: Module Browser UI</h3>
|
|
||||||
<ol>
|
|
||||||
<li>Navigate to <code>/modules/browse</code> in your application</li>
|
|
||||||
<li>Search for modules from <strong>library.silverlabs.uk</strong></li>
|
|
||||||
<li>Click "Install" on any module</li>
|
|
||||||
<li>Modules are downloaded and installed automatically</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h3>Option 3: Manual Installation</h3>
|
|
||||||
<div class="code-block"><code># Copy module DLLs to the modules directory
|
|
||||||
cp SomeModule.dll wwwroot/modules/
|
|
||||||
dotnet run
|
|
||||||
# Module is automatically discovered and loaded!</code></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Creating Modules Section -->
|
|
||||||
<div class="sdk-section">
|
|
||||||
<h2>🔧 Creating Modules</h2>
|
|
||||||
|
|
||||||
<h3>Using the Module Template</h3>
|
|
||||||
<div class="code-block"><code># Create a basic module
|
|
||||||
dotnet new silvershell-module -n MyModule
|
|
||||||
|
|
||||||
# Create a module with widgets
|
|
||||||
dotnet new silvershell-module -n MyModule --includeWidgets true
|
|
||||||
|
|
||||||
# Create a module with search provider
|
|
||||||
dotnet new silvershell-module -n MyModule --includeSearchProvider true
|
|
||||||
|
|
||||||
# Create a module with everything
|
|
||||||
dotnet new silvershell-module -n MyModule \
|
|
||||||
--includeWidgets true \
|
|
||||||
--includeSearchProvider true \
|
|
||||||
--includeTests true</code></div>
|
|
||||||
|
|
||||||
<h3>Module Structure</h3>
|
|
||||||
<p>The template creates an organized structure:</p>
|
|
||||||
<div class="code-block"><code>MyModule/
|
|
||||||
├── Configuration/
|
|
||||||
│ ├── ModuleMetadata.cs # Module identity and version
|
|
||||||
│ ├── EndpointConfiguration.cs # Navigation routes
|
|
||||||
│ └── WidgetConfiguration.cs # Dashboard widgets
|
|
||||||
├── Pages/
|
|
||||||
│ └── Index.razor # Razor pages
|
|
||||||
├── Components/
|
|
||||||
│ └── ... # Razor components
|
|
||||||
├── .gitlab-ci.yml # GitLab CI/CD
|
|
||||||
├── .github/workflows/
|
|
||||||
│ └── publish.yml # GitHub Actions
|
|
||||||
└── MyModuleMain.cs # Module entry point</code></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Publishing Modules Section -->
|
|
||||||
<div class="sdk-section">
|
|
||||||
<h2>🚀 Publishing Modules</h2>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<strong>CI/CD Included!</strong> The module template includes ready-to-use CI/CD pipelines for both GitLab and GitHub.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Automated Publishing (Recommended)</h3>
|
|
||||||
<p>The templates include CI/CD pipelines for automatic publishing:</p>
|
|
||||||
|
|
||||||
<ol class="steps-list">
|
|
||||||
<li>
|
|
||||||
<strong>Configure CI/CD variables</strong>
|
|
||||||
<div class="code-block"><code># In GitLab: Settings > CI/CD > Variables
|
|
||||||
# In GitHub: Settings > Secrets > Actions
|
|
||||||
|
|
||||||
# Add variable:
|
|
||||||
MODULE_REPO_TOKEN: [your token from library.silverlabs.uk]</code></div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Commit and push your code</strong>
|
|
||||||
<div class="code-block"><code>git add .
|
|
||||||
git commit -m "feat: Initial module implementation"
|
|
||||||
git push</code></div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Create a release tag</strong>
|
|
||||||
<div class="code-block"><code>git tag v1.0.0
|
|
||||||
git push --tags</code></div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Trigger publish</strong> from your CI/CD pipeline UI
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h3>Manual Publishing</h3>
|
|
||||||
<div class="code-block"><code># Build and package
|
|
||||||
dotnet pack --configuration Release -o dist/
|
|
||||||
|
|
||||||
# Upload to repository
|
|
||||||
curl -X POST "https://library.silverlabs.uk/api/modules/publish" \
|
|
||||||
-F "id=MyModule" \
|
|
||||||
-F "name=My Awesome Module" \
|
|
||||||
-F "version=1.0.0" \
|
|
||||||
-F "description=A great module" \
|
|
||||||
-F "author=Your Name" \
|
|
||||||
-F "package=@dist/MyModule.1.0.0.nupkg"</code></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Available Modules Section -->
|
|
||||||
<div class="sdk-section">
|
|
||||||
<h2>📚 Available Modules</h2>
|
|
||||||
<p>Browse and install modules from the SilverSHELL module repository:</p>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<strong>Module Repository:</strong> <a href="https://library.silverlabs.uk" target="_blank" style="color: #a78bfa;">library.silverlabs.uk</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Featured Modules:</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Auth.Login</strong> - User authentication and login UI</li>
|
|
||||||
<li><strong>Auth.Registration</strong> - User registration system</li>
|
|
||||||
<li><strong>Auth.UserManagement</strong> - User administration</li>
|
|
||||||
<li><strong>Auth.MyAccount</strong> - User profile management</li>
|
|
||||||
<li><strong>ModuleBrowser</strong> - Browse and install modules from the UI</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>Explore All Modules</h3>
|
|
||||||
<div class="code-block"><code># List all available modules via API
|
|
||||||
curl https://library.silverlabs.uk/api/modules
|
|
||||||
|
|
||||||
# Search for specific modules
|
|
||||||
curl https://library.silverlabs.uk/api/modules/search?q=auth</code></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Resources Section -->
|
|
||||||
<div class="sdk-section">
|
|
||||||
<h2>📖 Resources</h2>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Module Repository API:</strong> <a href="https://library.silverlabs.uk/api/modules" target="_blank" style="color: #a78bfa;">https://library.silverlabs.uk/api/modules</a></li>
|
|
||||||
<li><strong>Demo Application:</strong> <a href="https://demo.silverlabs.uk" target="_blank" style="color: #a78bfa;">https://demo.silverlabs.uk</a></li>
|
|
||||||
<li><strong>GitLab Repository:</strong> <a href="https://gitlab.silverlabs.uk/silverlabs/silvershell" target="_blank" style="color: #a78bfa;">GitLab</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Support Section -->
|
|
||||||
<div class="sdk-section">
|
|
||||||
<h2>💬 Support</h2>
|
|
||||||
<p>Need help? We're here for you:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Help Desk:</strong> <a href="https://helpdesk.silverlabs.uk" target="_blank" style="color: #a78bfa;">helpdesk.silverlabs.uk</a></li>
|
|
||||||
<li><strong>Issues:</strong> <a href="https://gitlab.silverlabs.uk/silverlabs/silvershell/-/issues" target="_blank" style="color: #a78bfa;">GitLab Issues</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href="/" class="back-link">← Back to SilverLabs Home</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="../script.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user