init: initial commit
@@ -0,0 +1,56 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||
{
|
||||
"name": "bonch-calendar",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-10.0",
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "lts",
|
||||
"pnpmVersion": "none",
|
||||
"nvmVersion": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"installDockerBuildx": true,
|
||||
"version": "latest",
|
||||
"dockerDashComposeVersion": "v2"
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [8000, 8080],
|
||||
// "portsAttributes": {
|
||||
// "5001": {
|
||||
// "protocol": "https"
|
||||
// }
|
||||
// }
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": {
|
||||
"api": "dotnet restore api/BonchCalendar.csproj",
|
||||
"app": "cd app && npm install"
|
||||
},
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"bierner.github-markdown-preview",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"github.vscode-github-actions",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"jock.svg",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"patcx.vscode-nuget-gallery",
|
||||
"saeris.markdown-github-alerts",
|
||||
"humao.rest-client"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
* @XFox111
|
||||
@@ -0,0 +1,102 @@
|
||||
name: "🐞 Bug Report"
|
||||
description: Create a report to help us improve the project
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "needs-triage"]
|
||||
assignees:
|
||||
- xfox111
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- type: textarea
|
||||
id: desc
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: e.g. The calendar is empty, even though timetable for my group is published.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Precisely describe minimal number of steps that make the bug to appear
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See '...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
placeholder: e.g. My timetable should be displayed in the calendar.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshot
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser name
|
||||
placeholder: e.g. Google Chrome
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: calendar
|
||||
attributes:
|
||||
label: Calendar service
|
||||
placeholder: e.g. Google Calendar, Outlook, etc.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: group
|
||||
attributes:
|
||||
label: Faculty, course number and group number
|
||||
description: |
|
||||
If you don't want to share your group publicly, say "PM", and send it to feedback@xfox111.net. Reference the issue number.
|
||||
placeholder: e.g. РТС, 2, РТ-31м
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: requested-help
|
||||
attributes:
|
||||
label: Are you willing to submit a PR for this issue?
|
||||
options:
|
||||
- "yes"
|
||||
- "no"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
|
||||
required: true
|
||||
- label: The provided reproduction is a minimal reproducible example of the bug.
|
||||
required: true
|
||||
@@ -0,0 +1,7 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/XFox111/bonch-calendar/discussions
|
||||
about: Use GitHub discussions to ask your questions.
|
||||
@@ -0,0 +1,62 @@
|
||||
name: "🚀 New feature proposal"
|
||||
description: Suggest a feature idea for this project
|
||||
title: "[Feature]: "
|
||||
labels: ["feature", "needs-triage"]
|
||||
assignees:
|
||||
- xfox111
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for your interest in the project and taking the time to fill out this feature report!
|
||||
|
||||
- type: textarea
|
||||
id: proposition
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: Describe the solution you'd like
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: justification
|
||||
attributes:
|
||||
label: Justification
|
||||
description: Is your feature request related to a problem? Please describe.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alts
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: Describe alternatives you've considered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: requested-help
|
||||
attributes:
|
||||
label: Are you willing to submit a PR for this issue?
|
||||
options:
|
||||
- "yes"
|
||||
- "no (I am aware that the project's author will not work on this issue)"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checkboxes
|
||||
attributes:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
|
||||
required: true
|
||||
@@ -0,0 +1,81 @@
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/app"
|
||||
target-branch: "main"
|
||||
assignees:
|
||||
- "XFox111"
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
groups:
|
||||
deps:
|
||||
patterns:
|
||||
- "*"
|
||||
exclude-patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
react:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
react-next:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
update-types:
|
||||
- major
|
||||
|
||||
- package-ecosystem: "nuget" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
target-branch: "main"
|
||||
assignees:
|
||||
- "XFox111"
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
target-branch: "main"
|
||||
assignees:
|
||||
- "XFox111"
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
target-branch: "main"
|
||||
assignees:
|
||||
- "XFox111"
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
target-branch: "main"
|
||||
assignees:
|
||||
- "XFox111"
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
open-pull-requests-limit: 20
|
||||
@@ -0,0 +1,6 @@
|
||||
## Description
|
||||
<!-- Put a detailed description of the pull request here (what does it change and how does it change?) -->
|
||||
|
||||
<!-- If you have an issue number, add it here with "resolves" prefix -->
|
||||
<!-- For example: -->
|
||||
<!-- resolves: #69420 -->
|
||||
@@ -0,0 +1,88 @@
|
||||
name: "Audit pipeline"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths-ignore:
|
||||
- '.devcontainer/*'
|
||||
- '.github/*'
|
||||
- '!.github/workflows/audit.yml'
|
||||
- '.vscode/*'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- 'assets/*'
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths-ignore:
|
||||
- '.devcontainer/*'
|
||||
- '.github/*'
|
||||
- '!.github/workflows/audit.yml'
|
||||
- '.vscode/*'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- 'assets/*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
api:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./api
|
||||
tags: ${{ github.repository }}-api:ci
|
||||
|
||||
- run: docker save ${{ github.repository }}:ci | gzip > api_image.tar.gz
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: api-image
|
||||
path: api_image.tar.gz
|
||||
|
||||
app:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./app
|
||||
tags: ${{ github.repository }}-app:ci
|
||||
|
||||
- run: docker save ${{ github.repository }}:ci | gzip > app_image.tar.gz
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: app-image
|
||||
path: app_image.tar.gz
|
||||
|
||||
app_audit:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- run: npm install
|
||||
working-directory: ./app
|
||||
|
||||
- run: npm run lint
|
||||
working-directory: ./app
|
||||
|
||||
- run: npm audit --audit-level=moderate
|
||||
working-directory: ./app
|
||||
|
||||
- run: npm audit --audit-level=moderate --json > audit_report.json
|
||||
working-directory: ./app
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: app-audit-report
|
||||
path: ./app/audit_report.json
|
||||
@@ -0,0 +1,110 @@
|
||||
name: "Release pipeline"
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
api:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}-api
|
||||
ghcr.io/${{ github.repository }}-api
|
||||
tags: |
|
||||
latest
|
||||
${{ github.ref_name }}
|
||||
|
||||
- name: "Login to Docker Hub"
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: "Login to GitHub Container Registry"
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./api
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
app:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}-app
|
||||
ghcr.io/${{ github.repository }}-app
|
||||
tags: |
|
||||
latest
|
||||
${{ github.ref_name }}
|
||||
|
||||
- name: "Login to Docker Hub"
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: "Login to GitHub Container Registry"
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./app
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
pages:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:latest
|
||||
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- run: npm install
|
||||
working-directory: ./app
|
||||
|
||||
- run: npm run build
|
||||
env:
|
||||
VITE_BACKEND_HOST: https://api.bonch.xfox111.net
|
||||
working-directory: ./app
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: "./app/dist"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"bierner.github-markdown-preview",
|
||||
"github.vscode-github-actions",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"patcx.vscode-nuget-gallery",
|
||||
"saeris.markdown-github-alerts",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"jock.svg",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"humao.rest-client"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
},
|
||||
{
|
||||
"name": "API: Launch BonchCalendar",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "api: build",
|
||||
"program": "${workspaceFolder}/api/bin/Debug/net10.0/BonchCalendar.dll",
|
||||
"cwd": "${workspaceFolder}/api",
|
||||
"stopAtEntry": false,
|
||||
"console": "internalConsole",
|
||||
"suppressJITOptimizations": true,
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_URLS": "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"editor.rulers": [
|
||||
{
|
||||
"column": 120
|
||||
}
|
||||
],
|
||||
"editor.insertSpaces": false,
|
||||
"files.insertFinalNewline": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "always"
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"files.trimFinalNewlines": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"todo-tree.filtering.excludeGlobs": [
|
||||
"**/node_modules/*/**",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "api: build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/api"
|
||||
},
|
||||
"args": [
|
||||
"build",
|
||||
"BonchCalendar.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "api: publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/api"
|
||||
},
|
||||
"args": [
|
||||
"publish",
|
||||
"BonchCalendar.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "api: watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"group": "test",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/api"
|
||||
},
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"BonchCalendar.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "app: install",
|
||||
"type": "npm",
|
||||
"script": "install",
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "app: build",
|
||||
"type": "npm",
|
||||
"script": "build",
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "app: dev",
|
||||
"type": "npm",
|
||||
"script": "dev",
|
||||
"group": "test",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "app: lint",
|
||||
"type": "npm",
|
||||
"script": "lint",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "app: preview",
|
||||
"type": "npm",
|
||||
"script": "preview",
|
||||
"group": "test",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "docker: build (app)",
|
||||
"type": "process",
|
||||
"command": "docker",
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app"
|
||||
},
|
||||
"args": [
|
||||
"build",
|
||||
"-t xfox111/bonch-calendar-app:latest",
|
||||
"."
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "docker: build (api)",
|
||||
"type": "process",
|
||||
"command": "docker",
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/api"
|
||||
},
|
||||
"args": [
|
||||
"build",
|
||||
"-t xfox111/bonch-calendar-api:latest",
|
||||
"."
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "docker: compose up",
|
||||
"type": "process",
|
||||
"command": "docker",
|
||||
"group": "test",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"args": [
|
||||
"compose",
|
||||
"up",
|
||||
"--build",
|
||||
"--force-recreate"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[opensource@xfox111.net](mailto:opensource@xfox111.net).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
> Contributor Covenant is released under the [Creative Commons Attribution 4.0 International Public License](https://github.com/EthicalSource/contributor_covenant/blob/release/LICENSE.md).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -0,0 +1,26 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
This document will guide you through the process of contributing to this project. We welcome contributions from the community and appreciate your efforts to improve it.
|
||||
|
||||
> [!NOTE]
|
||||
> This document is a work in progress. More information will be added later.
|
||||
|
||||
## Asking questions
|
||||
|
||||
## Submitting bug reports and feature requests
|
||||
|
||||
## Code contributions
|
||||
|
||||
### What should I know/learn before I start?
|
||||
|
||||
### Setting up development environment
|
||||
|
||||
#### With devcontainers
|
||||
|
||||
#### Without devcontainers
|
||||
|
||||
### Building the project
|
||||
|
||||
### Getting familiar with the codebase
|
||||
|
||||
### Submitting a pull request
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Eugene Fox
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,48 @@
|
||||
[](https://bonch.xfox111.net)
|
||||
[](https://github.com/xfox111/bonch-calendar/releases/latest)
|
||||
[](https://github.com/XFox111/bonch-calendar/commits/main)
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="assets/dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="assets/light.png">
|
||||
<img alt="Bonch.Calendar. Check your SPbSUT timetable in your calendar!">
|
||||
</picture>
|
||||
|
||||
A simple service that provides web calendars that you can subscribe to in your calendar application (Google Calendar, Apple Calendar, Outlook, etc.) and see your group's timetable there.
|
||||
|
||||
This is my farewell gift to the university.
|
||||
|
||||
## Demo
|
||||
|
||||
<!-- TODO: put demo here -->
|
||||
|
||||
## Q&A
|
||||
|
||||
### Q: I have questions. Where do I ask?
|
||||
|
||||
You can go to the [discussions tab](https://github.com/bonch-calendar/discussions) to ask questions or start a discussion.
|
||||
|
||||
But before that, check out the [FAQ section](https://bonch.xfox111.net/#faq) on the website, as your question may already be answered there.
|
||||
|
||||
### Q: I want to contribute to the project. How can I do that?
|
||||
|
||||
First, you can check out [open issues](https://github.com/bonch-calendar/issues) or [discussions](https://github.com/bonch-calendar/discussions) to see if there are any tasks you can help with.
|
||||
|
||||
If you already found one, or have an idea for a new feature or improvement, you can create a pull request with your changes.
|
||||
|
||||
The [contributing guidelines](CONTRIBUTING.md) contain all the information you need.
|
||||
|
||||
> [!NOTE]
|
||||
> Before starting to work on a new feature or bugfix, it's a good idea to open an issue first to discuss it with the maintainers. This way, you can ensure that your efforts align with the project's needs and avoid duplicating work.
|
||||
|
||||
### Q: I'd like to become a maintainer. How can I do that?
|
||||
|
||||
If you're interested in becoming a maintainer, please reach out to me via email at [eugene@xfox111.net](mailto:eugene@xfox111.net). You'll have to show that you're capable though.
|
||||
|
||||
---
|
||||
|
||||
[](https://bsky.app/profile/xfox111.net)
|
||||
[](https://github.com/xfox111)
|
||||
[](https://buymeacoffee.com/xfox111)
|
||||
|
||||
> ©2025 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/bonch-calendar/blob/main/LICENSE)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Security Policy
|
||||
|
||||
We as maintainers of this project are committed to maintaining the security of our software. We take security vulnerabilities seriously and will work to address them as quickly as possible.
|
||||
|
||||
We regularly run security audits and fix any security issues that are found. If you find a security issue, please report it to us as described below.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
You can report a security issue by going through [this link](https://github.com/XFox111/bonch-calendar/security/advisories/new)
|
||||
@@ -0,0 +1,482 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from `dotnet new gitignore`
|
||||
|
||||
# dotenv files
|
||||
.env
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Tye
|
||||
.tye/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
.idea/
|
||||
|
||||
##
|
||||
## Visual studio for Mac
|
||||
##
|
||||
|
||||
|
||||
# globs
|
||||
Makefile.in
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
config.make
|
||||
config.status
|
||||
aclocal.m4
|
||||
install-sh
|
||||
autom4te.cache/
|
||||
*.tar.gz
|
||||
tarballs/
|
||||
test-results/
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
@@ -0,0 +1,482 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from `dotnet new gitignore`
|
||||
|
||||
# dotenv files
|
||||
.env
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Tye
|
||||
.tye/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
.idea/
|
||||
|
||||
##
|
||||
## Visual studio for Mac
|
||||
##
|
||||
|
||||
|
||||
# globs
|
||||
Makefile.in
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
config.make
|
||||
config.status
|
||||
aclocal.m4
|
||||
install-sh
|
||||
autom4te.cache/
|
||||
*.tar.gz
|
||||
tarballs/
|
||||
test-results/
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.3.1" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
|
||||
<PackageReference Include="Ical.Net" Version="5.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
@Host = http://localhost:8080
|
||||
|
||||
GET {{Host}}/health
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Host}}/faculties
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET {{Host}}/groups
|
||||
?facultyId=56682
|
||||
&course=2
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
@groupId = 56606
|
||||
@facultyId = 50029
|
||||
GET {{Host}}/timetable/{{facultyId}}/{{groupId}}
|
||||
Accept: text/calendar
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BonchCalendar", "BonchCalendar.csproj", "{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{811C13A0-E5FC-452C-8628-AD36B9A8A7E2}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
|
||||
WORKDIR /build
|
||||
|
||||
ADD *.csproj .
|
||||
RUN dotnet restore
|
||||
|
||||
ADD . ./
|
||||
RUN dotnet publish --no-restore --configuration Release --output /out
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS prod
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /out/* .
|
||||
|
||||
ENTRYPOINT [ "dotnet", "BonchCalendar.dll" ]
|
||||
@@ -0,0 +1,26 @@
|
||||
using BonchCalendar.Services;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace BonchCalendar.Health;
|
||||
|
||||
public class ApiHealthCheck(ApiService groupService) : IHealthCheck
|
||||
{
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context, CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
Dictionary<int, string> faculties = await groupService.GetFacultiesListAsync();
|
||||
|
||||
if (faculties.Count > 0)
|
||||
return HealthCheckResult.Healthy();
|
||||
|
||||
return HealthCheckResult.Degraded(description: "Timetable website looks to be up, but returned an empty list of faculties.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy(description: "Timetable website appears to be down.", exception: ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BonchCalendar;
|
||||
using BonchCalendar.Health;
|
||||
using BonchCalendar.Services;
|
||||
using BonchCalendar.Utils;
|
||||
using HealthChecks.UI.Client;
|
||||
using Ical.Net;
|
||||
using Ical.Net.CalendarComponents;
|
||||
using Ical.Net.Serialization;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddValidation();
|
||||
builder.Services.AddProblemDetails(configure =>
|
||||
{
|
||||
configure.CustomizeProblemDetails = context =>
|
||||
{
|
||||
context.ProblemDetails.Extensions["traceId"] = context.HttpContext.TraceIdentifier;
|
||||
context.ProblemDetails.Extensions["naas_reason"] = new NaasReasons().GetReason();
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services
|
||||
.AddScoped<ApiService>()
|
||||
.AddScoped<ParsingService>();
|
||||
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck<ApiHealthCheck>("timetable_website");
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
options.AddDefaultPolicy(policy =>
|
||||
policy
|
||||
.WithMethods(["GET"])
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
)
|
||||
);
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
app.UseCors();
|
||||
app.UseStatusCodePages();
|
||||
app.MapOpenApi();
|
||||
|
||||
app.MapHealthChecks("/health", new HealthCheckOptions
|
||||
{
|
||||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
|
||||
});
|
||||
|
||||
ILogger<Program> logger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||
|
||||
app.MapGet("/faculties", async ([FromServices] ApiService apiService) =>
|
||||
{
|
||||
logger.LogInformation("Fetching faculties list.");
|
||||
Dictionary<int, string> faculties = await apiService.GetFacultiesListAsync();
|
||||
return Results.Ok(faculties);
|
||||
})
|
||||
.WithName("GetFaculties")
|
||||
.WithDescription("Gets the list of faculties.")
|
||||
.Produces<Dictionary<int, string>>(StatusCodes.Status200OK);
|
||||
|
||||
app.MapGet("/groups", async ([FromServices] ApiService apiService, int facultyId, [Range(1, 5)] int course) =>
|
||||
{
|
||||
logger.LogInformation("Fetching groups list for faculty {FacultyId} and course {Course}.", facultyId, course);
|
||||
Dictionary<int, string> groups = await apiService.GetGroupsListAsync(facultyId, course);
|
||||
return Results.Ok(groups);
|
||||
})
|
||||
.WithName("GetGroups")
|
||||
.WithDescription("Gets the list of groups for the specified faculty and course.")
|
||||
.Produces<Dictionary<int, string>>(StatusCodes.Status200OK)
|
||||
.ProducesValidationProblem();
|
||||
|
||||
app.MapGet("/timetable/{facultyId}/{groupId}", async (
|
||||
int facultyId, int groupId,
|
||||
[FromServices] ApiService apiService,
|
||||
[FromServices] ParsingService parsingService
|
||||
) =>
|
||||
{
|
||||
logger.LogInformation("Generating timetable for group {GroupId} of faculty {FacultyId}.", groupId, facultyId);
|
||||
string cacheFile = Path.Combine(Path.GetTempPath(), $"bonch_cal_{groupId}.ics");
|
||||
|
||||
if (File.Exists(cacheFile) && (DateTime.UtcNow - File.GetLastWriteTimeUtc(cacheFile)).TotalHours < 6)
|
||||
{
|
||||
if (args.Contains("--no-cache"))
|
||||
logger.LogWarning("Cache disabled via --no-cache, regenerating timetable for group {GroupId}.", groupId);
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Serving timetable for group {GroupId} from cache.", groupId);
|
||||
return Results.Text(await File.ReadAllTextAsync(cacheFile), contentType: "text/calendar");
|
||||
}
|
||||
}
|
||||
|
||||
DateTime semesterStartDate = await apiService.GetSemesterStartDateAsync(groupId);
|
||||
string groupName = (await apiService.GetGroupsListAsync(facultyId, 0))[groupId];
|
||||
|
||||
string classesRaw = await apiService.GetScheduleDocumentAsync(groupId, TimetableType.Classes);
|
||||
List<CalendarEvent> timetable = [.. parsingService.ParseGeneralTimetable(classesRaw, semesterStartDate, groupName)];
|
||||
|
||||
TimetableType[] types = [TimetableType.Attestations, TimetableType.Exams, TimetableType.ExamsForExtramural];
|
||||
foreach (TimetableType type in types)
|
||||
{
|
||||
classesRaw = await apiService.GetScheduleDocumentAsync(groupId, type);
|
||||
timetable.AddRange(parsingService.ParseExamTimetable(classesRaw, groupName));
|
||||
}
|
||||
|
||||
Calendar calendar = new();
|
||||
calendar.Properties.Add(new CalendarProperty("X-WR-CALNAME", groupName));
|
||||
calendar.Properties.Add(new CalendarProperty("X-WR-TIMEZONE", "Europe/Moscow"));
|
||||
calendar.Properties.Add(new CalendarProperty("REFRESH-INTERVAL;VALUE=DURATION", "PT6H"));
|
||||
calendar.Events.AddRange(timetable);
|
||||
calendar.AddTimeZone(new VTimeZone("Europe/Moscow"));
|
||||
string serialized = new CalendarSerializer().SerializeToString(calendar)!;
|
||||
|
||||
await File.WriteAllTextAsync(cacheFile, serialized);
|
||||
logger.LogInformation("Cached timetable for group {GroupId} to {CacheFile}.", groupId, cacheFile);
|
||||
return Results.Text(serialized, contentType: "text/calendar");
|
||||
})
|
||||
.WithName("GetTimetable")
|
||||
.WithDescription("Gets the iCal timetable for the specified group.")
|
||||
.Produces<string>(StatusCodes.Status200OK, "text/calendar")
|
||||
.ProducesValidationProblem();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:8080",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:8443;http://localhost:8080",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using AngleSharp.Html.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using BonchCalendar.Utils;
|
||||
|
||||
namespace BonchCalendar.Services;
|
||||
|
||||
public class ApiService
|
||||
{
|
||||
public async Task<Dictionary<int, string>> GetFacultiesListAsync() =>
|
||||
ParseListResponse(await SendRequestAsync(new()
|
||||
{
|
||||
["choice"] = "1", // "choice" is always "1" (idk why, don't ask me)
|
||||
["schet"] = GetCurrentSemesterId()
|
||||
}));
|
||||
|
||||
public async Task<Dictionary<int, string>> GetGroupsListAsync(int facultyId, int course) =>
|
||||
ParseListResponse(await SendRequestAsync(new()
|
||||
{
|
||||
["choice"] = "1",
|
||||
["schet"] = GetCurrentSemesterId(),
|
||||
["faculty"] = facultyId.ToString(), // Specifying faculty ID returns a list of groups
|
||||
["kurs"] = course.ToString() // Course number is actually optional, but filters out other groups. Can be set 0 to get all groups of the faculty.
|
||||
}));
|
||||
|
||||
public async Task<string> GetScheduleDocumentAsync(int groupId, TimetableType timetableType) =>
|
||||
await SendRequestAsync(new()
|
||||
{
|
||||
["schet"] = GetCurrentSemesterId(),
|
||||
["type_z"] = ((int)timetableType).ToString(),
|
||||
["group"] = groupId.ToString()
|
||||
});
|
||||
|
||||
public async Task<DateTime> GetSemesterStartDateAsync(int groupId)
|
||||
{
|
||||
using HttpClient client = new();
|
||||
string content = await client.GetStringAsync($"https://www.sut.ru/studentu/raspisanie/raspisanie-zanyatiy-studentov-ochnoy-i-vecherney-form-obucheniya?group={groupId}");
|
||||
|
||||
using IHtmlDocument doc = new HtmlParser().ParseDocument(content);
|
||||
string labelText = doc.QuerySelector("a#rasp-prev + div:nth-child(2) > span")!.TextContent;
|
||||
int weekNumber = int.Parse(ParserUtils.NumberRegex().Match(labelText).Value);
|
||||
DateTime currentDate = DateTime.Today;
|
||||
currentDate = currentDate
|
||||
.AddDays(-(int)currentDate.DayOfWeek + 1) // Move to Monday
|
||||
.AddDays(-7 * (weekNumber - 1)); // Move back to the first week
|
||||
|
||||
return currentDate;
|
||||
}
|
||||
|
||||
private static Dictionary<int, string> ParseListResponse(string responseContent) =>
|
||||
responseContent
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(item => item.Split(','))
|
||||
.ToDictionary(
|
||||
parts => int.Parse(parts[0]),
|
||||
parts => parts[1]
|
||||
);
|
||||
|
||||
public async Task<string> SendRequestAsync(Dictionary<string, string> formData)
|
||||
{
|
||||
HttpRequestMessage request = new(HttpMethod.Post, "https://cabinet.sut.ru/raspisanie_all_new.php")
|
||||
{
|
||||
Content = new FormUrlEncodedContent(formData)
|
||||
};
|
||||
|
||||
using HttpClient client = new(new HttpClientHandler
|
||||
{
|
||||
// Sometimes Bonch being Bonch just doesn't renew its SSL certificates properly
|
||||
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
|
||||
});
|
||||
|
||||
HttpResponseMessage response = await client.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
private static string GetCurrentSemesterId()
|
||||
{
|
||||
DateTime now = DateTime.Today;
|
||||
int currentSemester = now.Month is >= 8 or < 2
|
||||
? 1 // August through January - first semester
|
||||
: 2; // Everything else - second
|
||||
|
||||
int termStartYear = now.Year - 2000; // We need only last two digits (e.g. 25 for 2025)
|
||||
|
||||
if (now.Month < 8) // Before August means we are in the second semester of the previous academic year
|
||||
termStartYear--;
|
||||
|
||||
return $"205.{termStartYear}{termStartYear + 1}/{currentSemester}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
using BonchCalendar.Utils;
|
||||
using Ical.Net.CalendarComponents;
|
||||
using Ical.Net.DataTypes;
|
||||
|
||||
namespace BonchCalendar.Services;
|
||||
|
||||
public partial class ParsingService
|
||||
{
|
||||
public CalendarEvent[] ParseGeneralTimetable(string rawHtml, DateTime semesterStartDate, string groupName)
|
||||
{
|
||||
using IHtmlDocument doc = new HtmlParser().ParseDocument(rawHtml);
|
||||
|
||||
IHtmlCollection<IElement> rawClasses = doc.QuerySelectorAll(".pair");
|
||||
List<CalendarEvent> classes = [];
|
||||
|
||||
foreach (IElement classItem in rawClasses)
|
||||
{
|
||||
var (className, classType, professors, auditorium) = ParseBaseInfo(classItem);
|
||||
int weekday = int.Parse(classItem.GetAttribute("weekday")!);
|
||||
|
||||
string timeLabelText = classItem.ParentElement!.ParentElement!.Children[0].TextContent;
|
||||
Match timeMatch = ParserUtils.TimeLabelRegex().Match(timeLabelText);
|
||||
string number = timeMatch.Success ? timeMatch.Groups["number"].Value : timeLabelText;
|
||||
(TimeSpan startTime, TimeSpan endTime) = !timeMatch.Success ?
|
||||
ParserUtils.GetTimesFromLabel(timeLabelText) :
|
||||
(
|
||||
TimeSpan.Parse(timeMatch.Groups["start"].Value),
|
||||
TimeSpan.Parse(timeMatch.Groups["end"].Value)
|
||||
);
|
||||
|
||||
int[] weeks = [
|
||||
.. ParserUtils.NumberRegex().Matches(classItem.QuerySelector(".weeks")!.TextContent)
|
||||
.Select(i => int.Parse(i.Value))
|
||||
];
|
||||
|
||||
foreach (int week in weeks)
|
||||
{
|
||||
DateTime classDate = semesterStartDate
|
||||
.AddDays((week - 1) * 7) // Move to the correct week
|
||||
.AddDays(weekday - 1); // Move to the correct weekday
|
||||
|
||||
classes.Add(GetEvent(
|
||||
$"{number}. {className} ({classType})", auditorium,
|
||||
GetDescription(groupName, professors, auditorium, weeks),
|
||||
classDate, startTime, endTime));
|
||||
}
|
||||
}
|
||||
|
||||
return [.. classes];
|
||||
}
|
||||
|
||||
public CalendarEvent[] ParseExamTimetable(string rawHtml, string groupName)
|
||||
{
|
||||
using IHtmlDocument doc = new HtmlParser().ParseDocument(rawHtml);
|
||||
|
||||
IHtmlCollection<IElement> rawClasses = doc.QuerySelectorAll(".pair");
|
||||
List<CalendarEvent> classes = new(rawClasses.Count);
|
||||
|
||||
foreach (IElement classItem in rawClasses)
|
||||
{
|
||||
var (className, classType, professors, auditorium) = ParseBaseInfo(classItem);
|
||||
|
||||
DateTime classDate = DateTime.Parse(classItem.Children[0].ChildNodes[0].TextContent, CultureInfo.GetCultureInfo("ru-RU"));
|
||||
Match timeMatch = ParserUtils.ExamTimeRegex().Match(classItem.GetAttribute("pair")!);
|
||||
|
||||
if (!timeMatch.Success)
|
||||
timeMatch = ParserUtils.ExamTimeAltRegex().Match(classItem.GetAttribute("pair")!);
|
||||
|
||||
string number = timeMatch.Groups["number"].Success ?
|
||||
$"{timeMatch.Groups["number"].Value}. " : string.Empty;
|
||||
|
||||
TimeSpan startTime = TimeSpan.Parse(timeMatch.Groups["start"].Value.Replace('.', ':'));
|
||||
TimeSpan endTime = TimeSpan.Parse(timeMatch.Groups["end"].Value.Replace('.', ':'));
|
||||
|
||||
classes.Add(GetEvent(
|
||||
$"{number}{className} ({classType})", auditorium,
|
||||
GetDescription(groupName, professors, auditorium),
|
||||
classDate, startTime, endTime));
|
||||
}
|
||||
|
||||
return [.. classes];
|
||||
}
|
||||
|
||||
private static CalendarEvent GetEvent(string title, string auditorium, string description, DateTime date, TimeSpan startTime, TimeSpan endTime) =>
|
||||
new()
|
||||
{
|
||||
Summary = title,
|
||||
Description = description,
|
||||
Start = new CalDateTime(date.Add(startTime - TimeSpan.FromHours(3)).ToUniversalTime()),
|
||||
End = new CalDateTime(date.Add(endTime - TimeSpan.FromHours(3)).ToUniversalTime()),
|
||||
Location = auditorium
|
||||
};
|
||||
|
||||
private static string GetDescription(string groupName, string[] professors, string auditorium, int[]? weeks = null)
|
||||
{
|
||||
string str = $"""
|
||||
Группа: {groupName}
|
||||
Преподаватель(и):
|
||||
- {string.Join("\n- ", professors)}
|
||||
""";
|
||||
|
||||
if (weeks is not null && weeks.Length > 0)
|
||||
str += $"\nНедели: {string.Join(", ", weeks)}";
|
||||
|
||||
Match auditoriumMatch = ParserUtils.AuditoriumRegex().Match(auditorium);
|
||||
|
||||
if (!auditoriumMatch.Success)
|
||||
auditoriumMatch = ParserUtils.AuditoriumAltRegex().Match(auditorium);
|
||||
|
||||
if (auditoriumMatch.Success)
|
||||
str += "\n\n" + $"""
|
||||
ГУТ.Навигатор:
|
||||
https://nav.sut.ru/?cab=k{auditoriumMatch.Groups["wing"].Value}-{auditoriumMatch.Groups["room"].Value}
|
||||
""";
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
private static (string className, string classType, string[] professors, string auditorium) ParseBaseInfo(IElement classElement)
|
||||
{
|
||||
string className = classElement.QuerySelector(".subect")!.TextContent;
|
||||
string classType = classElement.QuerySelector(".type")!.TextContent
|
||||
.Replace("(", string.Empty).Replace(")", string.Empty).Trim();
|
||||
|
||||
string[] professors = classElement.QuerySelector(".teacher[title]")!.GetAttribute("title")
|
||||
!.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
string auditorium = classElement.QuerySelector(".aud")!.TextContent
|
||||
.Replace("ауд.:", string.Empty).Replace(';', ',').Trim();
|
||||
|
||||
return (className, classType, professors, auditorium);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace BonchCalendar;
|
||||
|
||||
public enum TimetableType
|
||||
{
|
||||
Classes = 1,
|
||||
Exams = 2,
|
||||
ExamsForExtramural = 4,
|
||||
Attestations = 14
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BonchCalendar.Utils;
|
||||
|
||||
public static partial class ParserUtils
|
||||
{
|
||||
public static (TimeSpan startTime, TimeSpan endTime) GetTimesFromLabel(string label)
|
||||
{
|
||||
(string startTime, string endTime) = label switch
|
||||
{
|
||||
"1" => ("9:00", "10:35"),
|
||||
"2" => ("10:45", "12:20"),
|
||||
"3" => ("13:00", "14:35"),
|
||||
"4" => ("14:45", "16:20"),
|
||||
"5" => ("16:30", "18:05"),
|
||||
"6" => ("18:15", "19:50"),
|
||||
"7" => ("20:00", "21:35"),
|
||||
"Ф1" => ("9:00", "10:30"),
|
||||
"Ф2" => ("10:30", "12:00"),
|
||||
"Ф3" => ("12:00", "13:30"),
|
||||
"Ф4" => ("13:30", "15:00"),
|
||||
"Ф5" => ("15:00", "16:30"),
|
||||
"Ф6" => ("16:30", "18:00"),
|
||||
"Ф7" => ("18:00", "19:30"),
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
return (TimeSpan.Parse(startTime), TimeSpan.Parse(endTime));
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^(?<number>\S+)\s\((?<start>\d+:\d+)-(?<end>\d+:\d+)\)$")]
|
||||
public static partial Regex TimeLabelRegex();
|
||||
|
||||
[GeneratedRegex(@"\d+")]
|
||||
public static partial Regex NumberRegex();
|
||||
|
||||
[GeneratedRegex(@"^(?<room>\d+),\sБ22\/(?<wing>\d)$")]
|
||||
public static partial Regex AuditoriumRegex();
|
||||
|
||||
[GeneratedRegex(@"^(?<room>\d+),\sпр\.Большевиков,22,к\.(?<wing>\d)$")]
|
||||
public static partial Regex AuditoriumAltRegex();
|
||||
|
||||
[GeneratedRegex(@"^(?<number>\d)\s\((?<start>\d+\.\d+)-(?<end>\d+\.\d+)\)$")]
|
||||
public static partial Regex ExamTimeRegex();
|
||||
|
||||
[GeneratedRegex(@"^(?<start>\d+:\d+)-(?<end>\d+:\d+)$")]
|
||||
public static partial Regex ExamTimeAltRegex();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,30 @@
|
||||
# Use the official Node.js image as the base image
|
||||
FROM node:latest AS builder
|
||||
|
||||
ARG API_HOST=https://api.bonch.xfox111.net
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the package.json and package-lock.json files to the working directory
|
||||
COPY package*.json ./
|
||||
|
||||
# Install the app dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy the app source code to the working directory
|
||||
COPY . .
|
||||
|
||||
RUN echo "VITE_BACKEND_HOST=${API_HOST}" > .env.production
|
||||
|
||||
# Build the app
|
||||
RUN npm run build
|
||||
|
||||
# Use the official Nginx image as the base image
|
||||
FROM nginx:latest AS prod
|
||||
|
||||
# Copy the build output to replace the default Nginx contents
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Expose port 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,27 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["dist"]),
|
||||
{
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules:
|
||||
{
|
||||
"@typescript-eslint/no-unused-vars": "warn"
|
||||
}
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,55 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/bonch-calendar.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#FFFFFF" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#4D4D4D" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
|
||||
<title>Бонч.Календарь</title>
|
||||
|
||||
<meta name="description" content="Смотри расписание СПбГУТ в своем календаре" />
|
||||
<link rel="author" href="https://xfox111.net" />
|
||||
<meta name="author" content="Eugene Fox" />
|
||||
<meta name="keywords"
|
||||
content="Bonch,SPbSUT,Бонч,СПбГУТ,расписание,календарь,пары,schedule,timetable,classes,calendar,Eugene Fox,Michael Gordeev,Mikhail Gordeev" />
|
||||
<link rel="canonical" href="https://bonch.xfox111.net" />
|
||||
<meta property="og:title" content="Бонч.Календарь" />
|
||||
<meta property="og:description" content="Смотри расписание СПбГУТ в своем календаре" />
|
||||
<meta property="og:site_name" content="bonch.xfox111.net" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="ru_RU" />
|
||||
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="600" />
|
||||
<meta property="og:image" content="https://bonch.xfox111.net/opengraph.png" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@xfox111" />
|
||||
<meta name="twitter:title" content="Бонч.Календарь" />
|
||||
<meta name="twitter:description" content="Смотри расписание СПбГУТ в своем календаре" />
|
||||
|
||||
<meta name="twitter:image:type" content="image/png" />
|
||||
<meta name="twitter:image:width" content="1200" />
|
||||
<meta name="twitter:image:height" content="600" />
|
||||
<meta name="twitter:image" content="https://bonch.xfox111.net/opengraph.png" />
|
||||
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16" />
|
||||
<link rel="icon" href="/bonch-calendar.svg" type="image/svg+xml" sizes="any" />
|
||||
<link rel="apple-touch-icon" href="/apple-icon.png" type="image/png" sizes="180x180" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "bonch-calendar",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.72.7",
|
||||
"@fluentui/react-icons": "^2.0.314",
|
||||
"@fluentui/react-motion-components-preview": "^0.14.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-localization": "^2.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.3",
|
||||
"vite": "^7.2.2"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="100%" height="100%" viewBox="0 0 2048 2048" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="Bonch" serif:id="Bonch">
|
||||
<g transform="matrix(20.48,0,0,20.48,-7359.8976,-1024)">
|
||||
<path
|
||||
d="M424.82,150C436.27,137.11 444,116.54 444,100.77C444,90 440.87,70.55 424.82,50L422.77,51C432.28,67.33 433.71,81.44 433.71,100.77C433.71,104.66 433.92,133.23 422.98,148.77L424.82,150Z"
|
||||
style="fill:rgb(246,139,31);fill-rule:nonzero;" />
|
||||
<path
|
||||
d="M398.82,142.19C408.48,131.31 415,114 415,100.65C415,91.56 412.32,75.15 398.78,57.81L397.06,58.66C405.06,72.44 406.29,84.34 406.29,100.66C406.29,103.94 406.46,128.04 397.23,141.17L398.82,142.19Z"
|
||||
style="fill:rgb(246,139,31);fill-rule:nonzero;" />
|
||||
<path
|
||||
d="M376.15,134.37C384.02,125.52 389.36,111.37 389.36,100.53C389.36,93.12 387.18,79.75 376.15,65.63L374.74,66.31C381.28,77.54 382.26,87.24 382.26,100.53C382.26,103.2 382.4,122.84 374.88,133.53L376.15,134.37Z"
|
||||
style="fill:rgb(246,139,31);fill-rule:nonzero;" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 169 KiB |
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
©2025 Eugene Fox. All rights reserved.
|
||||
|
||||
This graphic file is excempt from the MIT license that applies to the rest of the
|
||||
project. You are not allowed to use it in your own projects without explicit permission
|
||||
from the author.
|
||||
|
||||
See https://github.com/XFox111/my-website/blob/main/COPYING for more information.
|
||||
-->
|
||||
<svg id="Layer_5" data-name="Layer 5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 1000">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1,
|
||||
.cls-2,
|
||||
.cls-3 {
|
||||
stroke-width: 0px;
|
||||
}
|
||||
|
||||
.cls-1,
|
||||
.cls-4 {
|
||||
fill: #ff7545;
|
||||
}
|
||||
|
||||
.cls-2,
|
||||
.cls-6 {
|
||||
fill: #242424;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
stroke-width: 12px;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.cls-6,
|
||||
.cls-4 {
|
||||
stroke: #242424;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.cls-6,
|
||||
.cls-4 {
|
||||
stroke-linecap: round;
|
||||
stroke-width: 8px;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.laptop {
|
||||
fill: #424242;
|
||||
stroke: #424242;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.laptop {
|
||||
fill: #d6d6d6;
|
||||
stroke: #d6d6d6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<path class="cls-1"
|
||||
d="M1656,996.17c-62.9,0-124.32-15.87-177.6-45.9-48.31-27.23-88.43-65.09-116.63-109.96,42.24,16.15,87.02,24.34,133.29,24.34,191.24,0,346.83-142.61,346.83-317.91,0-49.29-17.77-79.71-40.27-118.22-.25-.44-.51-.87-.77-1.31,56.26,25.15,103.09,59.13,135.92,98.71,38.75,46.72,58.4,100.56,58.4,160,0,82.81-35.24,160.68-99.22,219.27-64.08,58.67-149.29,90.99-239.96,90.99Z" />
|
||||
<path class="cls-2"
|
||||
d="M1810.27,435.77c21.37,10.21,41.26,21.71,59.35,34.32,24.99,17.43,46.59,37.03,64.2,58.27,38.17,46.02,57.52,99.03,57.52,157.56,0,41.28-8.83,81.34-26.25,119.04-16.85,36.47-40.98,69.24-71.73,97.4-30.8,28.2-66.67,50.34-106.62,65.82-41.4,16.04-85.39,24.17-130.75,24.17-62.25,0-123.01-15.7-175.72-45.4-44.28-24.95-81.59-58.94-108.99-99.08,39.45,13.69,80.98,20.62,123.77,20.62,193.35,0,350.66-144.33,350.66-321.74,0-46.31-15.25-76.1-35.44-110.97M1791.69,419.07c25.27,43.77,46.37,74.72,46.37,127.67,0,173.46-153.57,314.08-343,314.08-50.83,0-99.07-10.14-142.46-28.3,57.52,99.6,171.79,167.48,303.4,167.48,189.44,0,343-140.62,343-314.08,0-126.92-88.98-217.31-207.31-266.85h0Z" />
|
||||
</g>
|
||||
<path class="cls-4"
|
||||
d="M1850.7,210.11c40.63,49.7,33.28,122.93-16.42,163.56-49.7,40.63-174.15,75.16-214.78,25.46-40.63-49.7,17.94-164.81,67.64-205.44,49.7-40.63,122.93-33.28,163.56,16.42Z" />
|
||||
<g>
|
||||
<path class="cls-1"
|
||||
d="M1141.23,996c-107.8,0-211.68-30.24-292.51-85.15-34.04-23.13-63.19-50-86.62-79.87-30.95-39.44-50.89-82.52-59.27-128.07,2.74-4.13,13.24-18.52,34.99-33.02,23.72-15.81,66.05-35,133.04-36.62,3.24-.07,6.3-.11,9.33-.11,43.77,0,86.56,14.88,130.79,45.49,38.84,26.88,73.68,62.33,104.42,93.61,24.59,25.02,47.83,48.66,69.78,64.67,62.27,45.4,122.66,67.87,162.35,78.74,26.53,7.27,47.34,10.45,59.7,11.84-35.66,20.71-74.9,37.05-116.83,48.62-47.77,13.19-97.96,19.88-149.17,19.88Z" />
|
||||
<path class="cls-2"
|
||||
d="M880.19,637.16c42.93,0,84.97,14.65,128.51,44.78,38.53,26.66,73.23,61.97,103.85,93.12,24.71,25.14,48.06,48.89,70.28,65.1,62.76,45.75,123.63,68.41,163.65,79.36,19.52,5.35,36,8.51,48.36,10.37-32.55,17.79-67.94,32.01-105.51,42.38-47.43,13.09-97.26,19.73-148.11,19.73-54.46,0-107.6-7.59-157.95-22.55-48.57-14.43-93.08-35.26-132.31-61.91-33.7-22.89-62.54-49.48-85.72-79.03-30.18-38.46-49.75-80.41-58.19-124.72,8.21-11.64,51.24-63.8,163.88-66.52,3.21-.07,6.24-.11,9.25-.11M880.19,629.16c-3.2,0-6.34.04-9.43.11-132.17,3.19-172.15,72.82-172.15,72.82,8.48,47.56,29.48,92.03,60.34,131.36,23.74,30.26,53.31,57.47,87.52,80.71,78.67,53.44,181.82,85.84,294.76,85.84,105.42,0,202.3-28.24,278.72-75.46,0,0-28.21-.92-71.36-12.74-43.16-11.81-101.26-34.52-161.05-78.11-76.68-55.9-167.65-204.53-307.35-204.53h0Z" />
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3"
|
||||
d="M760.28,828.65c-29.91-38.81-49.23-81.08-57.46-125.74,2.74-4.13,13.24-18.52,34.99-33.02,23.38-15.59,64.87-34.46,130.24-36.54l51.71,133.53-159.48,61.77Z" />
|
||||
<path class="cls-2"
|
||||
d="M865.35,637.45l49.24,127.14-152.95,59.24c-28.13-37.18-46.48-77.52-54.58-120.04,8.07-11.44,49.8-62.07,158.29-66.35M870.76,629.27c-132.17,3.19-172.15,72.82-172.15,72.82,8.48,47.56,29.48,92.03,60.34,131.36l165.99-64.29-54.18-139.89h0Z" />
|
||||
</g>
|
||||
<rect class="cls-5 laptop" x="1219.11" y="766.83" width="270.32" height="9.95"
|
||||
transform="translate(304.74 -380.67) rotate(18)" />
|
||||
<rect class="cls-5 laptop" x="1059.2" y="596.18" width="270.32" height="9.95"
|
||||
transform="translate(1479.19 -705.28) rotate(75.58)" />
|
||||
<path class="cls-6"
|
||||
d="M1666.04,416.09c1.06-29.87-22.29-54.95-52.17-56.01-2.32-.08-4.6,0-6.85.2.75,15,4.9,28.4,13.47,38.89,10.4,12.71,26.28,19.9,44.99,22.9.29-1.96.48-3.95.55-5.98Z" />
|
||||
<path class="cls-4"
|
||||
d="M1851.96,176.25c-29.01-25.87-78.84-33.24-78.84-33.24,0,0,37.28-38.45,83.99-62.26,46.65-23.78,102.73-32.92,102.73-32.92,0,0-26.34,46.29-43.76,93.12-19.06,51.23-22.35,98.62-22.35,98.62,0,0-13.99-38.55-41.77-63.33Z" />
|
||||
<ellipse class="cls-2" cx="1700.37" cy="301.32" rx="10.19" ry="17.93"
|
||||
transform="translate(271.38 1270.89) rotate(-44.21)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1,34 @@
|
||||
import { FluentProvider, makeStyles, type Theme } from "@fluentui/react-components";
|
||||
import { type ReactElement } from "react";
|
||||
import { useTheme } from "./hooks/useTheme";
|
||||
import MainView from "./views/MainView";
|
||||
import FaqView from "./views/FaqView";
|
||||
import DedicatedView from "./views/DedicatedView";
|
||||
import FooterView from "./views/FooterView";
|
||||
|
||||
export default function App(): ReactElement
|
||||
{
|
||||
const theme: Theme = useTheme();
|
||||
const cls = useStyles();
|
||||
|
||||
return (
|
||||
<FluentProvider theme={ theme }>
|
||||
<main className={ cls.root }>
|
||||
<MainView />
|
||||
<FaqView />
|
||||
<DedicatedView />
|
||||
<FooterView />
|
||||
</main>
|
||||
</FluentProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
alignItems: "center",
|
||||
minHeight: "100vh"
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { webDarkTheme, webLightTheme, type Theme } from "@fluentui/react-components";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const media: MediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const getTheme = (isDark: boolean) => isDark ? darkTheme : lightTheme;
|
||||
|
||||
const baseTheme: Partial<Theme> =
|
||||
{
|
||||
fontFamilyBase: "\"Fira Sans\", sans-serif",
|
||||
colorBrandForeground1: "#f68b1f",
|
||||
colorBrandStroke1: "#f68b1f",
|
||||
colorBrandForegroundLink: "#f68b1f",
|
||||
colorBrandForegroundLinkHover: "#c36e18",
|
||||
colorBrandForegroundLinkPressed: "#a95f15",
|
||||
colorBrandStroke2Contrast: "#FDE6CE",
|
||||
colorBrandBackground: "#f68b1f",
|
||||
colorBrandBackgroundHover: "#c36e18",
|
||||
colorNeutralForeground2BrandHover: "#c36e18",
|
||||
colorBrandBackgroundPressed: "#a95f15",
|
||||
colorCompoundBrandStroke: "#f68b1f",
|
||||
colorCompoundBrandStrokePressed: "#a95f15"
|
||||
};
|
||||
|
||||
const lightTheme: Theme =
|
||||
{
|
||||
...webLightTheme, ...baseTheme,
|
||||
colorNeutralForeground1: "#000000",
|
||||
colorNeutralForeground2: "#4D4D4D"
|
||||
};
|
||||
|
||||
const darkTheme: Theme =
|
||||
{
|
||||
...webDarkTheme, ...baseTheme,
|
||||
colorNeutralBackground2: "#4D4D4D"
|
||||
};
|
||||
|
||||
export function useTheme(): Theme
|
||||
{
|
||||
const [theme, setTheme] = useState<Theme>(getTheme(media.matches));
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const updateTheme = (args: MediaQueryListEvent) => setTheme(getTheme(args.matches));
|
||||
media.addEventListener("change", updateTheme);
|
||||
return () => media.removeEventListener("change", updateTheme);
|
||||
}, []);
|
||||
|
||||
return theme;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function useTimeout(timeout: number): [boolean, () => void]
|
||||
{
|
||||
const [isActive, setActive] = useState<boolean>(false);
|
||||
|
||||
const trigger = useCallback(() =>
|
||||
{
|
||||
if (isActive)
|
||||
return;
|
||||
|
||||
setActive(true);
|
||||
setTimeout(() => setActive(false), timeout);
|
||||
}, [timeout, isActive]);
|
||||
|
||||
return [isActive, trigger];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
body
|
||||
{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
font-family: "Fira Sans", sans-serif;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, ul, p
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes scaleUpFade
|
||||
{
|
||||
0%
|
||||
{
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
100%
|
||||
{
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
import strings from "./utils/strings.ts";
|
||||
|
||||
const preferredLanguages = navigator.languages.map(lang => lang.split("-")[0].toLocaleLowerCase());
|
||||
|
||||
if (
|
||||
(preferredLanguages.includes("ru")&& !window.location.pathname.startsWith("/en")) ||
|
||||
window.location.pathname.startsWith("/ru")
|
||||
)
|
||||
strings.setLanguage("ru");
|
||||
else
|
||||
{
|
||||
strings.setLanguage("en");
|
||||
document.title = strings.formatString(strings.title_p1, strings.title_p2) as string;
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
export const fetchFaculties = async (): Promise<[string, string][]> =>
|
||||
{
|
||||
const res = await fetch(import.meta.env.VITE_BACKEND_HOST + "/faculties");
|
||||
return Object.entries(await res.json());
|
||||
};
|
||||
|
||||
export const fetchGroups = async (facultyId: string, course: number): Promise<[string, string][]> =>
|
||||
{
|
||||
const res = await fetch(`${import.meta.env.VITE_BACKEND_HOST}/groups?facultyId=${facultyId}&course=${course}`);
|
||||
return Object.entries(await res.json());
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Link } from "@fluentui/react-components";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
const extLink = (url: string, text: string): ReactElement =>
|
||||
<Link href={ url } target="_blank" rel="noreferrer">{ text }</Link>;
|
||||
|
||||
export default extLink;
|
||||
@@ -0,0 +1,124 @@
|
||||
import LocalizedStrings from "react-localization";
|
||||
|
||||
const strings = new LocalizedStrings({
|
||||
en:
|
||||
{
|
||||
// MainView.tsx
|
||||
title_p1: "Bonch.{0}",
|
||||
title_p2: "Calendar",
|
||||
subtitle_p1: "Check your SPbSUT classes in {0} calendar",
|
||||
subtitle_p2: "your",
|
||||
pickFaculty: "1. Pick your faculty",
|
||||
pickCourse: "2. Pick your course",
|
||||
pickGroup: "3. Pick your group",
|
||||
pickGroup_empty: "No groups are available for the selected course",
|
||||
subscribe: "4. Subscribe to the calendar",
|
||||
copy: "Copy link",
|
||||
or: "or",
|
||||
download: "Download .ics file",
|
||||
cta: "Like the service? Tell your classmates!",
|
||||
|
||||
// FaqView.tsx
|
||||
faq_h2: "Frequently asked questions",
|
||||
question1_h3: "How do I save timetable to my Outlook/Google calendar?",
|
||||
answer1_p1: "Once you picked your group, copy the generated link. Then, in your calendar app subscirbe to a new calendar using that link. Here're some guides:",
|
||||
answer1_li1: "Google Calendar",
|
||||
answer1_li2: "Outlook",
|
||||
answer1_li3: "Apple iCloud",
|
||||
answer1_p2: "Note that subscribing to a web calendar is available only on desktop versions of Google and Outlook. But once you subscribe, the timetable will be available on all your devices.",
|
||||
question2_h3: "The timetable can change. Do I need to re-import the calendar every time?",
|
||||
answer2_p1: "Unless you imported the calendar from file, no. Subscribed calendars update automatically. On our end, calendars update every six hours.",
|
||||
question3_h3: "My group/faculty doesn't appear in the list. How do I get my timetable?",
|
||||
answer3_p1: "If your faculty or group is missing, it probably means that the timetable for it is not yet published. Try again later.",
|
||||
answer3_p2: "If you already have a calendar link, it will work once the timetable is published.",
|
||||
question4_h3: "Do I need to re-import my calendar at the start of each semester/year?",
|
||||
answer4_p1: "No. The generated calendar link is valid indefinitely and will always point to the latest timetable for your group.",
|
||||
answer4_p2: "That being said, if your group or faculty changes their name, you might need to generate a new link, as the group and faculty IDs might change.",
|
||||
question5_h3: "Does the calendar show timetable from past semesters?",
|
||||
answer5_p1: "No. The calendar contains only the current semester's timetable. Once you enter a new semester, all past events will disappear.",
|
||||
answer5_p2: "If you want to keep past semesters' timetables, consider downloading them as files at the end of each semester.",
|
||||
question6_h3: "Something doesn't work (timetable doesn't show, the website is broken, etc.). How do I report this?",
|
||||
answer6_p1: "You can file an issue on project's {0} or contact me {1} (the former is preferred).",
|
||||
answer6_p1_link1: "GitHub page",
|
||||
answer6_p1_link2: "via email",
|
||||
answer6_p2: "Note that I am no longer a student and work on this project in my spare time. So if you want to get a fix quickly, consider submitting a pull request yourself. You can find all the necessary information on project's {0}.",
|
||||
answer6_p2_link: "GitHub page",
|
||||
question7_h3: "I want a propose a new feature. Do I file it on GitHub as well?",
|
||||
answer7_p1: "I do not accept any feature requests for this project. However, if you want to propose a new feature, then yes, you can still file an issue on project's {0} and maybe someone else will implement it.",
|
||||
answer7_p1_link: "GitHub page",
|
||||
answer7_p2: "The other way is to implement the feature yourself and submit a pull request. I do welcome contributions.",
|
||||
question8_h3: "GUT.Schedule app doesn't work anymore. Will it be fixed?",
|
||||
answer8_p1: "GUT.Schedule application is no longer supported. This project is a successor to that app, so please use it instead.",
|
||||
answer8_p2: "That being said, the GUT.Schedule app is open source as well, so if you'd like to tinker with it, you can find its source code {0}.",
|
||||
answer8_p2_link: "on GitHub",
|
||||
|
||||
// DedicatedView.tsx
|
||||
dedicated_h2: "Dedicated to memory of Scientific and educational center \"Technologies of informational and educational systems\"",
|
||||
dedicated_p: "Always in our hearts ❤️",
|
||||
|
||||
// FooterView.tsx
|
||||
footer_p1: "Made with ☕ and ❤️{0}by {1}",
|
||||
footer_p2: "Eugene Fox"
|
||||
},
|
||||
ru:
|
||||
{
|
||||
// MainView.tsx
|
||||
title_p1: "Бонч.{0}",
|
||||
title_p2: "Календарь",
|
||||
subtitle_p1: "Смотри расписание СПбГУТ в {0} календаре",
|
||||
subtitle_p2: "своем",
|
||||
pickFaculty: "1. Выбери свой факультет",
|
||||
pickCourse: "2. Выбери свой курс",
|
||||
pickGroup: "3. Выбери свою группу",
|
||||
pickGroup_empty: "Нет доступных групп для выбранного курса",
|
||||
subscribe: "4. Подпишись на календарь",
|
||||
copy: "Скопировать ссылку",
|
||||
or: "или",
|
||||
download: "Скачай .ics файл",
|
||||
cta: "Понравился сервис? Расскажи одногруппникам!",
|
||||
|
||||
// FaqView.tsx
|
||||
faq_h2: "Часто задаваемые вопросы",
|
||||
question1_h3: "Как сохранить расписание в Outlook/Google календарь?",
|
||||
answer1_p1: "После того, как вы выбрали свою группу, скопируйте сгенерированную ссылку. Затем в своем календаре подпишитесь на новый календарь, используя эту ссылку. Вот несколько инструкций:",
|
||||
answer1_li1: "Google Календарь",
|
||||
answer1_li2: "Outlook",
|
||||
answer1_li3: "Apple Календарь",
|
||||
answer1_p2: "Обратите внимание, что в Google и Outlook подписаться на веб-календарь можно только в веб-версиях этих сервисов. Но после этого расписание будет доступно на всех ваших устройствах.",
|
||||
question2_h3: "Расписание может меняться. Нужно ли мне импортировать календарь каждый раз?",
|
||||
answer2_p1: "Если вы не импортировали календарь из файла, то нет. Календари на которые вы подписаны обновляются автоматически. С нашей стороны, календари обновляются каждые шесть часов.",
|
||||
question3_h3: "Моя группа/факультет не отображается в списке. Как мне получить свое расписание?",
|
||||
answer3_p1: "Если ваш факультет или группа отсутствует, скорее всего, расписание для них еще не опубликовано. Попробуйте позже.",
|
||||
answer3_p2: "Если у вас уже есть ссылка на календарь, можете использовать ее. Расписание появится в календаре сразу как только оно будет опубликовано.",
|
||||
question4_h3: "Нужно ли мне повторно импортировать календарь в начале каждого семестра/года?",
|
||||
answer4_p1: "Нет. Сгенерированная ссылка на календарь действительна бессрочно и всегда будет указывать на актуальное расписание для вашей группы.",
|
||||
answer4_p2: "Однако, если ваша группа или факультет изменили свое название, возможно, вам придется сгенерировать новую ссылку, так как идентификаторы группы или факультета могли также измениться.",
|
||||
question5_h3: "Показывает ли календарь расписание из прошлых семестров?",
|
||||
answer5_p1: "Нет. Календарь содержит только расписание текущего семестра. Как только начнется новый семестр, все прошедшие события исчезнут.",
|
||||
answer5_p2: "Если вы все же хотите сохранить расписания прошлых семестров, вы можете скачивать их в виде файлов в конце каждого семестра.",
|
||||
question6_h3: "Что-то не работает (расписание не отображается, сайт сломан и т.д.). Как мне об этом сообщить?",
|
||||
answer6_p1: "Вы можете создать задачу на {0} проекта или связаться со мной {1} (первый вариант предпочтительнее).",
|
||||
answer6_p1_link1: "странице GitHub",
|
||||
answer6_p1_link2: "по электронной почте",
|
||||
answer6_p2: "Обратите внимание, что я больше не являюсь студентом и работаю над этим проектом в свое свободное время. Поэтому, если вы хотите быстро получить исправление, вы можете самостоятельно создать пул реквест. Вся необходимая информация доступна на {0} проекта.",
|
||||
answer6_p2_link: "странице GitHub",
|
||||
question7_h3: "Я хочу предложить новую функцию. Это также делается через GitHub?",
|
||||
answer7_p1: "Я не принимаю запросы на добавление функций для этого проекта. Однако, если вы хотите предложить новую функцию, то да, вы можете создать задачу на {0} проекта, и, возможно, кто-то другой ее реализует.",
|
||||
answer7_p1_link: "странице GitHub",
|
||||
answer7_p2: "Другой способ - реализовать функцию самостоятельно и отправить пул реквест. Я приветствую сторонний вклад в проект.",
|
||||
question8_h3: "Приложение ГУТ.Расписание больше не работает. Его починят?",
|
||||
answer8_p1: "Приложение ГУТ.Расписание больше не поддерживается. Этот проект является его преемником.",
|
||||
answer8_p2: "Тем не менее, ГУТ.Расписание также имеет открытый исходный код, поэтому, если вы хотите с ним поэкспериментировать, вы можете найти его {0}.",
|
||||
answer8_p2_link: "на GitHub",
|
||||
|
||||
// DedicatedView.tsx
|
||||
dedicated_h2: "Посвящается памяти научно-образовательного центра \"ТИОС\"",
|
||||
dedicated_p: "Навсегда в наших сердцах ❤️",
|
||||
|
||||
// FooterView.tsx
|
||||
footer_p1: "Сделано с ☕ и ❤️,{0}{1}",
|
||||
footer_p2: "Евгений Лис"
|
||||
}
|
||||
});
|
||||
|
||||
export default strings;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Body1, Body2, makeStyles, tokens } from "@fluentui/react-components";
|
||||
import type { ReactElement } from "react";
|
||||
import strings from "../utils/strings";
|
||||
|
||||
export default function DedicatedView(): ReactElement
|
||||
{
|
||||
const cls = useStyles();
|
||||
|
||||
return (
|
||||
<section className={ cls.root }>
|
||||
<Body2 as="h2" align="center">{ strings.dedicated_h2 }</Body2>
|
||||
<Body1 as="p" align="center">{ strings.dedicated_p }</Body1>
|
||||
<a href="https://www.sut.ru/bonchnews/science/07-11-2022-pobedy-studentov-i-aspirantov-spbgut-na-radiofeste-2022" target="_blank" rel="noreferrer">
|
||||
<img src="/tios.jpg" className={ cls.image } />
|
||||
</a>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
boxSizing: "border-box",
|
||||
flexFlow: "column",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingVerticalL,
|
||||
padding: `200px ${tokens.spacingHorizontalM}`
|
||||
},
|
||||
image:
|
||||
{
|
||||
width: "100%",
|
||||
maxWidth: "600px",
|
||||
borderRadius: tokens.borderRadiusMedium,
|
||||
boxShadow: tokens.shadow16
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
const useStyles_FaqView = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
justifyContent: "center",
|
||||
gap: tokens.spacingVerticalXXXL,
|
||||
width: "100%",
|
||||
maxWidth: "1200px",
|
||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingVerticalM}`,
|
||||
userSelect: "text",
|
||||
boxSizing: "border-box",
|
||||
marginBottom: tokens.spacingVerticalXXXL
|
||||
},
|
||||
question:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalM
|
||||
},
|
||||
answer:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalSNudge,
|
||||
padding: `${tokens.spacingVerticalNone} ${tokens.spacingHorizontalM}`
|
||||
}
|
||||
});
|
||||
|
||||
export default useStyles_FaqView;
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Body1, Subtitle1, Title3 } from "@fluentui/react-components";
|
||||
import type { ReactElement } from "react";
|
||||
import useStyles_FaqView from "./FaqView.styles";
|
||||
import extLink from "../utils/extLink";
|
||||
import strings from "../utils/strings";
|
||||
|
||||
const GITHUB_REPO = "https://github.com/xfox111/bonch-calendar";
|
||||
const GITHUB_ISSUES = "https://github.com/xfox111/bonch-calendar/issues";
|
||||
const GOOGLE_HELP = "https://support.google.com/calendar/answer/37100";
|
||||
const OUTLOOK_HELP = "https://support.microsoft.com/office/import-or-subscribe-to-a-calendar-in-outlook-com-or-outlook-on-the-web-cff1429c-5af6-41ec-a5b4-74f2c278e98c";
|
||||
const APPLE_HELP = "https://support.apple.com/en-us/102301";
|
||||
const EMAIL = "mailto:feedback@xfox111.net";
|
||||
const GUT_SCHEDULE_REPO = "https://github.com/xfox111/GUTSchedule";
|
||||
|
||||
export default function FaqView(): ReactElement
|
||||
{
|
||||
const cls = useStyles_FaqView();
|
||||
|
||||
return (
|
||||
<section className={ cls.root }>
|
||||
<Title3 align="center" as="h2" id="faq">{ strings.faq_h2 }</Title3>
|
||||
|
||||
<div id="faq1" className={ cls.question }>
|
||||
<Subtitle1 as="h3">{ strings.question1_h3 }</Subtitle1>
|
||||
<div className={ cls.answer }>
|
||||
<Body1 as="p">{ strings.answer1_p1 }</Body1>
|
||||
<ul>
|
||||
<li>{ extLink(GOOGLE_HELP, strings.answer1_li1) }</li>
|
||||
<li>{ extLink(OUTLOOK_HELP, strings.answer1_li2) }</li>
|
||||
<li>{ extLink(APPLE_HELP, strings.answer1_li3) }</li>
|
||||
</ul>
|
||||
<Body1 as="p">{ strings.answer1_p2 }</Body1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="faq2" className={ cls.question }>
|
||||
<Subtitle1 as="h3">{ strings.question2_h3 }</Subtitle1>
|
||||
<div className={ cls.answer }>
|
||||
<Body1 as="p">{ strings.answer2_p1 }</Body1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="faq3" className={ cls.question }>
|
||||
<Subtitle1 as="h3">{ strings.question3_h3 }</Subtitle1>
|
||||
<div className={ cls.answer }>
|
||||
<Body1 as="p">{ strings.answer3_p1 }</Body1>
|
||||
<Body1 as="p">{ strings.answer3_p2 }</Body1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="faq4" className={ cls.question }>
|
||||
<Subtitle1 as="h3">{ strings.question4_h3 }</Subtitle1>
|
||||
<div className={ cls.answer }>
|
||||
<Body1 as="p">{ strings.answer4_p1 }</Body1>
|
||||
<Body1 as="p">{ strings.answer4_p2 }</Body1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="faq5" className={ cls.question }>
|
||||
<Subtitle1 as="h3">{ strings.question5_h3 }</Subtitle1>
|
||||
<div className={ cls.answer }>
|
||||
<Body1 as="p">{ strings.answer5_p1 }</Body1>
|
||||
<Body1 as="p">{ strings.answer5_p2 }</Body1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="faq6" className={ cls.question }>
|
||||
<Subtitle1 as="h3">{ strings.question6_h3 }</Subtitle1>
|
||||
<div className={ cls.answer }>
|
||||
<Body1 as="p">
|
||||
{ strings.formatString(
|
||||
strings.answer6_p1,
|
||||
extLink(GITHUB_ISSUES, strings.answer6_p1_link1),
|
||||
extLink(EMAIL, strings.answer6_p1_link2)
|
||||
) }
|
||||
</Body1>
|
||||
<Body1 as="p">
|
||||
{ strings.formatString(strings.answer6_p2, extLink(GITHUB_REPO, strings.answer6_p2_link)) }
|
||||
</Body1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="faq7" className={ cls.question }>
|
||||
<Subtitle1 as="h3">{ strings.question7_h3 }</Subtitle1>
|
||||
<div className={ cls.answer }>
|
||||
<Body1 as="p">
|
||||
{ strings.formatString(strings.answer7_p1, extLink(GITHUB_REPO, strings.answer7_p1_link)) }
|
||||
</Body1>
|
||||
<Body1 as="p">{ strings.answer7_p2}</Body1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="faq8" className={ cls.question }>
|
||||
<Subtitle1 as="h3">{ strings.question8_h3 }</Subtitle1>
|
||||
<div className={ cls.answer }>
|
||||
<Body1 as="p">{ strings.answer8_p1 }</Body1>
|
||||
<Body1 as="p">
|
||||
{ strings.formatString(strings.answer8_p2, extLink(GUT_SCHEDULE_REPO, strings.answer8_p2_link)) }
|
||||
</Body1>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Body1, makeStyles } from "@fluentui/react-components";
|
||||
import type { ReactElement } from "react";
|
||||
import extLink from "../utils/extLink";
|
||||
import strings from "../utils/strings";
|
||||
|
||||
const MY_WEBSITE = "https://xfox111.net";
|
||||
|
||||
export default function FooterView(): ReactElement
|
||||
{
|
||||
const cls = useStyles();
|
||||
|
||||
return (
|
||||
<footer className={ cls.root }>
|
||||
<div className={ cls.imageContainer }>
|
||||
<Body1 as="p" className={ cls.caption }>
|
||||
{strings.formatString(strings.footer_p1, <br />, extLink(MY_WEBSITE, strings.footer_p2))}
|
||||
</Body1>
|
||||
<img src="/footer.svg" />
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "end"
|
||||
},
|
||||
imageContainer:
|
||||
{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
maxWidth: "400px",
|
||||
},
|
||||
caption:
|
||||
{
|
||||
position: "absolute",
|
||||
top: "24px",
|
||||
left: "72px",
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { makeStyles, tokens, shorthands } from "@fluentui/react-components";
|
||||
|
||||
|
||||
const useStyles_MainView = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalXXXL,
|
||||
justifyContent: "center",
|
||||
minHeight: "90vh",
|
||||
alignItems: "center",
|
||||
padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalL}`,
|
||||
|
||||
"& p":
|
||||
{
|
||||
textAlign: "center"
|
||||
}
|
||||
},
|
||||
highlight:
|
||||
{
|
||||
color: tokens.colorBrandForeground1
|
||||
},
|
||||
courseButton:
|
||||
{
|
||||
minWidth: "48px",
|
||||
borderRadius: tokens.borderRadiusNone,
|
||||
borderRightWidth: 0,
|
||||
|
||||
"&:first-of-type":
|
||||
{
|
||||
borderStartStartRadius: tokens.borderRadiusCircular,
|
||||
borderEndStartRadius: tokens.borderRadiusCircular
|
||||
},
|
||||
|
||||
"&:last-of-type":
|
||||
{
|
||||
borderStartEndRadius: tokens.borderRadiusCircular,
|
||||
borderEndEndRadius: tokens.borderRadiusCircular,
|
||||
borderRightWidth: tokens.strokeWidthThin,
|
||||
},
|
||||
},
|
||||
stack:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
alignItems: "center",
|
||||
gap: tokens.spacingVerticalSNudge
|
||||
},
|
||||
form:
|
||||
{
|
||||
gap: tokens.spacingVerticalL
|
||||
},
|
||||
copiedStyle:
|
||||
{
|
||||
color: tokens.colorStatusSuccessForeground1 + " !important",
|
||||
...shorthands.borderColor(tokens.colorStatusSuccessBorder1 + " !important")
|
||||
},
|
||||
field:
|
||||
{
|
||||
width: "250px"
|
||||
},
|
||||
copyIcon:
|
||||
{
|
||||
animationName: "scaleUpFade",
|
||||
animationDuration: tokens.durationFast,
|
||||
animationTimingFunction: tokens.curveEasyEaseMax
|
||||
},
|
||||
hidden:
|
||||
{
|
||||
pointerEvents: "none"
|
||||
},
|
||||
truncatedText:
|
||||
{
|
||||
overflowX: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}
|
||||
});
|
||||
|
||||
export default useStyles_MainView;
|
||||
@@ -0,0 +1,167 @@
|
||||
import { LargeTitle, Subtitle1, Label, Dropdown, Button, Subtitle2, Body1, Option } from "@fluentui/react-components";
|
||||
import { mergeClasses, useArrowNavigationGroup } from "@fluentui/react-components";
|
||||
import type { SelectionEvents, OptionOnSelectData } from "@fluentui/react-components";
|
||||
import { Copy24Regular, ArrowDownload24Regular, Checkmark24Regular } from "@fluentui/react-icons";
|
||||
import { Slide, Stagger } from "@fluentui/react-motion-components-preview";
|
||||
import { use, useCallback, useMemo, useState, type ReactElement } from "react";
|
||||
import useTimeout from "../hooks/useTimeout";
|
||||
import useStyles_MainView from "./MainView.styles";
|
||||
import { fetchFaculties, fetchGroups } from "../utils/api";
|
||||
import strings from "../utils/strings";
|
||||
|
||||
const facultiesPromise = fetchFaculties();
|
||||
|
||||
const getEntryOrEmpty = (entries: [string, string][], key: string): string =>
|
||||
entries.find(i => i[0] === key)?.[1] ?? "";
|
||||
|
||||
export default function MainView(): ReactElement
|
||||
{
|
||||
const faculties: [string, string][] = use(facultiesPromise);
|
||||
const [facultyId, setFacultyId] = useState<string>("");
|
||||
|
||||
const courses: number[] = useMemo(() => facultyId == "56682" ? [1, 2] : [1, 2, 3, 4, 5], [facultyId]);
|
||||
const [course, setCourse] = useState<number>(0);
|
||||
|
||||
const [groups, setGroups] = useState<[string, string][] | null>(null);
|
||||
const [groupId, setGroupId] = useState<string>("");
|
||||
|
||||
const icalUrl = useMemo(() => `${import.meta.env.VITE_BACKEND_HOST}/timetable/${facultyId}/${groupId}`, [groupId, facultyId]);
|
||||
|
||||
const [showCta, setShowCta] = useState<boolean>(false);
|
||||
const [copyActive, triggerCopy] = useTimeout(3000);
|
||||
|
||||
const navAttributes = useArrowNavigationGroup({ axis: "horizontal" });
|
||||
const cls = useStyles_MainView();
|
||||
|
||||
const copyLink = useCallback((): void =>
|
||||
{
|
||||
navigator.clipboard.writeText(icalUrl);
|
||||
triggerCopy();
|
||||
setShowCta(true);
|
||||
}, [icalUrl, triggerCopy]);
|
||||
|
||||
const onFacultySelect = useCallback((_: SelectionEvents, data: OptionOnSelectData): void =>
|
||||
{
|
||||
if (data.optionValue === facultyId)
|
||||
return;
|
||||
|
||||
setFacultyId(data.optionValue!);
|
||||
setCourse(0);
|
||||
setGroupId("");
|
||||
setGroups(null);
|
||||
}, [facultyId]);
|
||||
|
||||
const onCourseSelect = useCallback((courseNumber: number): void =>
|
||||
{
|
||||
if (courseNumber === course)
|
||||
return;
|
||||
|
||||
setCourse(courseNumber);
|
||||
setGroupId("");
|
||||
setGroups(null);
|
||||
fetchGroups(facultyId, courseNumber).then(setGroups);
|
||||
}, [course, facultyId]);
|
||||
|
||||
return (
|
||||
<section className={ cls.root }>
|
||||
<header className={ cls.stack }>
|
||||
<LargeTitle as="h1">
|
||||
{ strings.formatString(strings.title_p1, <span className={ cls.highlight }>{ strings.title_p2 }</span>) }
|
||||
</LargeTitle>
|
||||
<Subtitle1 as="p">
|
||||
{ strings.formatString(strings.subtitle_p1, <span className={ cls.highlight }>{ strings.subtitle_p2 }</span>) }
|
||||
</Subtitle1>
|
||||
</header>
|
||||
<div className={ mergeClasses(cls.stack, cls.form) }>
|
||||
<Slide visible appear>
|
||||
<div className={ cls.stack }>
|
||||
<Label htmlFor="faculty">{ strings.pickFaculty }</Label>
|
||||
<Dropdown id="faculty"
|
||||
value={ getEntryOrEmpty(faculties, facultyId) }
|
||||
onOptionSelect={ onFacultySelect }
|
||||
className={ cls.field }
|
||||
positioning={ { pinned: true, position: "below" } }
|
||||
button={
|
||||
<span className={ cls.truncatedText }>{ getEntryOrEmpty(faculties, facultyId) }</span>
|
||||
}>
|
||||
|
||||
{ faculties.map(([id, name]) =>
|
||||
<Option key={ id } value={ id }>{ name }</Option>
|
||||
) }
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Slide>
|
||||
<Slide visible={ facultyId !== "" }>
|
||||
<div className={ mergeClasses(cls.stack, facultyId === "" && cls.hidden) }>
|
||||
<Label>{ strings.pickCourse }</Label>
|
||||
<div { ...navAttributes }>
|
||||
{ courses.map(i =>
|
||||
<Button key={ i }
|
||||
className={ cls.courseButton }
|
||||
appearance={ course === i ? "primary" : "secondary" }
|
||||
onClick={ () => onCourseSelect(i) }>
|
||||
|
||||
{ i }
|
||||
</Button>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
</Slide>
|
||||
<Slide visible={ course !== 0 && groups !== null }>
|
||||
<div className={ mergeClasses(cls.stack, course === 0 && cls.hidden) }>
|
||||
<Label as="label" htmlFor="group">{ strings.pickGroup }</Label>
|
||||
<Dropdown id="group"
|
||||
className={ cls.field }
|
||||
positioning={ { pinned: true, position: "below" } }
|
||||
value={ getEntryOrEmpty(groups ?? [], groupId) }
|
||||
onOptionSelect={ (_, e) => setGroupId(e.optionValue!) }>
|
||||
|
||||
{ groups?.map(([id, name]) =>
|
||||
<Option key={ id } value={ id }>{ name }</Option>
|
||||
) }
|
||||
{ (groups?.length ?? 0) < 1 &&
|
||||
<Option disabled>{ strings.pickGroup_empty }</Option>
|
||||
}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Slide>
|
||||
</div>
|
||||
<div className={ cls.stack }>
|
||||
<Stagger visible={ groupId !== "" }>
|
||||
<Slide>
|
||||
<div className={ mergeClasses(cls.stack, groupId === "" && cls.hidden) }>
|
||||
<Subtitle2>{ strings.subscribe }</Subtitle2>
|
||||
<Button
|
||||
onClick={ copyLink }
|
||||
className={ mergeClasses(cls.field, copyActive && cls.copiedStyle) }
|
||||
iconPosition="after"
|
||||
title={ strings.copy }
|
||||
icon={ copyActive
|
||||
? <Checkmark24Regular className={ cls.copyIcon } />
|
||||
: <Copy24Regular className={ cls.copyIcon } />
|
||||
}>
|
||||
|
||||
<span className={ cls.truncatedText }>{ icalUrl }</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Slide>
|
||||
<Slide>
|
||||
<div className={ mergeClasses(cls.stack, groupId === "" && cls.hidden) }>
|
||||
<Body1>{ strings.or }</Body1>
|
||||
<Button as="a"
|
||||
appearance="subtle" icon={ <ArrowDownload24Regular /> }
|
||||
onClick={ () => setShowCta(true) }
|
||||
href={ icalUrl }>
|
||||
|
||||
{ strings.download }
|
||||
</Button>
|
||||
</div>
|
||||
</Slide>
|
||||
</Stagger>
|
||||
</div>
|
||||
<Slide visible={ showCta }>
|
||||
<Subtitle2 as="p">{ strings.cta }</Subtitle2>
|
||||
</Slide>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": [
|
||||
"ES2023"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
api:
|
||||
image: xfox111/bonch-calendar-api:latest
|
||||
build:
|
||||
context: ./api
|
||||
ports:
|
||||
- 8080:8080
|
||||
|
||||
app:
|
||||
image: xfox111/bonch-calendar-app:latest
|
||||
build:
|
||||
context: ./app
|
||||
args:
|
||||
- API_HOST=http://localhost:8080
|
||||
ports:
|
||||
- 8000:80
|
||||
depends_on:
|
||||
- api
|
||||