Archived
Private
Public Access
1
0

finished schedule page + added substitution functions

This commit is contained in:
2023-04-29 14:01:27 +02:00
parent 20660cd746
commit 227af36c05
18 changed files with 584 additions and 11 deletions

View File

@@ -8,7 +8,7 @@ namespace BetterIServ.Backend.Controllers;
[ApiController] [ApiController]
[Route("iserv")] [Route("iserv")]
public class AuthController : ControllerBase { public class IServController : ControllerBase {
[HttpPost("login")] [HttpPost("login")]
public async Task<ActionResult<AuthKeys>> GetAuthKeysV2([FromBody] Credentials credentials) { public async Task<ActionResult<AuthKeys>> GetAuthKeysV2([FromBody] Credentials credentials) {

View File

@@ -12,6 +12,27 @@ export class IServService {
public keys?: AuthKeys; public keys?: AuthKeys;
public backend: string = "http://localhost:5273"; public backend: string = "http://localhost:5273";
public courseNames: {[id: string]: string} = {
["Bi"]: "Biologie",
["Ch"]: "Chemie",
["Ma"]: "Mathe",
["Ph"]: "Physik",
["De"]: "Deutsch",
["Ek"]: "Erdkunde",
["En"]: "Englisch",
["PW"]: "Politik",
["Sn"]: "Spanisch",
["If"]: "Informatik",
["Sp"]: "Sport",
["WN"]: "Werte und Normen",
["La"]: "Latein",
["Re"]: "Religion",
["Ge"]: "Geschichte",
["Ku"]: "Kunst",
["Sf"]: "Seminarfach",
["DS"]: "Darstellendes Spiel",
};
constructor(private client: HttpClient) { constructor(private client: HttpClient) {
const data = localStorage.getItem("userdata"); const data = localStorage.getItem("userdata");
if (data != null) { if (data != null) {
@@ -42,6 +63,11 @@ export class IServService {
} }
} }
public logout() {
delete this.userdata;
delete this.keys;
}
public async getKeys(): Promise<AuthKeys> { public async getKeys(): Promise<AuthKeys> {
const keys = await firstValueFrom(this.client.post<AuthKeys>(this.backend + "/iserv/login", this.userdata)); const keys = await firstValueFrom(this.client.post<AuthKeys>(this.backend + "/iserv/login", this.userdata));
localStorage.setItem("keys", JSON.stringify(keys)); localStorage.setItem("keys", JSON.stringify(keys));
@@ -52,9 +78,34 @@ export class IServService {
try { try {
return (await firstValueFrom(this.client.post<{value: string[]}>(this.backend + "/iserv/groups?domain=" + this.userdata.domain, this.keys))).value; return (await firstValueFrom(this.client.post<{value: string[]}>(this.backend + "/iserv/groups?domain=" + this.userdata.domain, this.keys))).value;
} catch { } catch {
await this.getKeys(); const keys = await this.getKeys();
return (await firstValueFrom(this.client.post<{value: string[]}>(this.backend + "/iserv/groups?domain=" + this.userdata.domain, this.keys))).value; return (await firstValueFrom(this.client.post<{value: string[]}>(this.backend + "/iserv/groups?domain=" + this.userdata.domain, keys))).value;
} }
} }
public async getCoursesAndClass(groups?: string[]): Promise<{class: string, courses: string[]}> {
if (groups == undefined) {
groups = await this.getGroups();
}
const result: {class: string, courses: string[]} = {class: undefined, courses: []};
const classNames = groups.filter(group => group.startsWith("Klasse ") && !group.includes("."));
if (classNames.length != 0) {
result.class = classNames[0].replace("Klasse ", "");
}else {
const grades = groups.filter(group => group.startsWith("Jahrgang ") && !group.includes("."));
if (grades.length != 0) {
result.class = grades[0].replace("Jahrgang ", "").toUpperCase();
}
}
for (let group of groups) {
if (!group.includes(".") || !group.toLowerCase().startsWith("q")) continue;
result.courses.push(group.split(".")[1]);
}
return result;
}
} }

View File

@@ -1,6 +1,6 @@
<ion-app> <ion-app>
<ion-split-pane contentId="main-content"> <ion-split-pane contentId="main-content">
<ion-menu contentId="main-content" type="overlay" *ngIf="router.url != '/login'"> <ion-menu contentId="main-content" type="overlay" [ngClass]="{'hide': router.url == '/login'}">
<ion-content> <ion-content>
<ion-list> <ion-list>
<ion-list-header>BetterIServ</ion-list-header> <ion-list-header>BetterIServ</ion-list-header>

View File

@@ -107,3 +107,7 @@ ion-note {
ion-item.selected { ion-item.selected {
--color: var(--ion-color-primary); --color: var(--ion-color-primary);
} }
.hide {
display: none;
}

View File

@@ -28,7 +28,8 @@ export class AppComponent {
} }
public logout() { public logout() {
localStorage.removeItem("userdata"); localStorage.clear();
this.iserv.logout();
this.router.navigate(["login"]); this.router.navigate(["login"]);
} }

View File

@@ -26,4 +26,12 @@ export const routes: Routes = [
path: 'substitution', path: 'substitution',
loadComponent: () => import('./pages/substitution/substitution.page').then( m => m.SubstitutionPage) loadComponent: () => import('./pages/substitution/substitution.page').then( m => m.SubstitutionPage)
}, },
{
path: 'tasks',
loadComponent: () => import('./pages/tasks/tasks.page').then( m => m.TasksPage)
},
{
path: 'schedule',
loadComponent: () => import('./pages/schedule/schedule.page').then( m => m.SchedulePage)
},
]; ];

View File

@@ -0,0 +1,16 @@
export interface Course {
id: string;
name: string;
short: string;
color: string;
}
export type Lesson = {course: string, room: string, week?: 'a' | 'b' | 'all'};
export interface Timetable {
mon: Lesson[];
tue: Lesson[];
wed: Lesson[];
thu: Lesson[];
fri: Lesson[];
}

View File

@@ -6,6 +6,7 @@
ion-card { ion-card {
width: 90%; width: 90%;
max-width: 700px;
ion-card-content { ion-card-content {
display: flex; display: flex;

View File

@@ -0,0 +1,185 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Stundenplan</ion-title>
<ion-buttons slot="end">
<ion-button (click)="onEditOrAdd()"><ion-icon ios="add-circle-outline" md="add-circle-sharp"></ion-icon></ion-button>
<ion-modal #courseModal (willDismiss)="updateOrCreateCourse($event)">
<ng-template>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="courseModal.dismiss(null, 'cancel')">Abbrechen</ion-button>
</ion-buttons>
<ion-title>{{currentCourse?.name || "Neuer Kurs"}}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="courseModal.dismiss({id: id.value, short: short.value, name: name.value, color: color.value}, 'confirm')" [strong]="true">Fertig</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding course-content">
<ion-item>
<ion-label position="stacked">Farbe</ion-label>
<ion-select aria-label="Farbe" interface="action-sheet" [value]="colors[0].val" #color>
<ion-select-option *ngFor="let color of colors" [value]="color.val">
{{color.name}}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked">Name</ion-label>
<ion-input aria-label="Name" type="text" [value]="currentCourse?.name" #name/>
</ion-item>
<ion-item>
<ion-label position="stacked">Kürzel</ion-label>
<ion-input aria-label="Kürzel" type="text" [value]="currentCourse?.short" #short/>
</ion-item>
<ion-item>
<ion-label position="stacked">Identifikator</ion-label>
<ion-input aria-label="Identifikator" type="text" [value]="currentCourse?.id" #id/>
</ion-item>
<ion-button *ngIf="currentCourse != undefined" (click)="courseModal.dismiss(null, 'delete')" color="danger">Kurs löschen</ion-button>
</ion-content>
</ng-template>
</ion-modal>
<ion-modal #tableModal (didDismiss)="updateOrCreateLesson($event)">
<ng-template>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="tableModal.dismiss(null, 'cancel')">Abbrechen</ion-button>
</ion-buttons>
<ion-title>{{currentLesson?.lesson.course || "Neue Stunde"}}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="tableModal.dismiss({lesson: {course: course.value, room: room.value, week: week.value}, day: day.value, time: lesson.value}, 'confirm')" [strong]="true">Fertig</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding course-content">
<ion-item>
<ion-label position="stacked">Kurs</ion-label>
<ion-select aria-label="Kurs" interface="action-sheet" [value]="currentLesson?.lesson.course || courses[0].id" #course>
<ion-select-option *ngFor="let course of courses" [value]="course.id">
{{course.name}}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked">Wochentyp</ion-label>
<ion-select aria-label="Wochentyp" interface="action-sheet" [value]="currentLesson?.lesson.week || 'all'" #week>
<ion-select-option value="all">Immer</ion-select-option>
<ion-select-option value="a">Woche A</ion-select-option>
<ion-select-option value="b">Woche B</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked">Raum</ion-label>
<ion-input aria-label="Raum" type="text" [value]="currentLesson?.lesson.room" #room/>
</ion-item>
<ion-item>
<ion-label position="stacked">Wochentag</ion-label>
<ion-select aria-label="Wochentag" interface="action-sheet" [value]="currentLesson?.day || 'mon'" #day>
<ion-select-option *ngFor="let dayName of ['mon', 'tue', 'wed', 'thu', 'fri']" [value]="dayName">
<ion-label *ngIf="dayName == 'mon'">Montag</ion-label>
<ion-label *ngIf="dayName == 'tue'">Dinstag</ion-label>
<ion-label *ngIf="dayName == 'wed'">Mittwoch</ion-label>
<ion-label *ngIf="dayName == 'thu'">Donnerstag</ion-label>
<ion-label *ngIf="dayName == 'fri'">Freitag</ion-label>
</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked">Stunde</ion-label>
<ion-select aria-label="Stunde" interface="action-sheet" [value]="currentLesson?.time || 0" #lesson>
<ion-select-option *ngFor="let lessonName of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" [value]="lessonName">
{{lessonName + 1}}
</ion-select-option>
</ion-select>
</ion-item>
<ion-button *ngIf="currentLesson != undefined" color="danger" (click)="tableModal.dismiss(null, 'delete')">Stunde löschen</ion-button>
</ion-content>
</ng-template>
</ion-modal>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Stundenplan</ion-title>
</ion-toolbar>
</ion-header>
<section class="courses" *ngIf="showCourses">
<ion-item><ion-label>Meine Kurse</ion-label></ion-item>
<div class="course-container">
<ion-card *ngFor="let course of courses" (click)="onEditOrAdd(course)">
<ion-card-header>
<span
class="icon ion-text-center"
[style]="'--background: var(--ion-color-' + course.color + '); --foreground: var(--ion-color-' + course.color + '-contrast)'"
>
{{course.short}}
</span>
</ion-card-header>
<ion-card-content class="ion-text-center">
<ion-label>{{course.name}}</ion-label><br>
<ion-label>{{course.id}}</ion-label>
</ion-card-content>
</ion-card>
</div>
</section>
<section class="timetable" *ngIf="!showCourses">
<div *ngFor="let day of ['mon', 'tue', 'wed', 'thu', 'fri']" class="ion-text-center">
<ion-label *ngIf="day == 'mon'">Mo</ion-label>
<ion-label *ngIf="day == 'tue'">Di</ion-label>
<ion-label *ngIf="day == 'wed'">Mi</ion-label>
<ion-label *ngIf="day == 'thu'">Do</ion-label>
<ion-label *ngIf="day == 'fri'">Fr</ion-label>
<ion-card *ngFor="let lesson of timetable[day] | week; let i = index" [ngClass]="{'hide': lesson == undefined}" (click)="onEditOrAdd(undefined, {lesson, day, time: i})">
<ion-card-header>
<span
class="icon ion-text-center"
[style]="'--background: var(--ion-color-' + findCourse(lesson?.course)?.color + '); --foreground: var(--ion-color-' + findCourse(lesson?.course)?.color + '-contrast)'"
>
{{findCourse(lesson?.course)?.short || "&#10240; &#x2800;"}}
</span>
</ion-card-header>
<ion-card-content class="ion-text-center">
<ion-label>{{lesson?.room || "&#10240; &#x2800;"}}</ion-label>
</ion-card-content>
</ion-card>
</div>
</section>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-grid>
<ion-row>
<ion-col>
<ion-tab-button (click)="showCourses = false" [ngClass]="{'active': !showCourses}">
<ion-icon ios="grid-outline" md="grid-sharp" />
Stundenplan
</ion-tab-button>
</ion-col>
<ion-col>
<ion-tab-button (click)="showCourses = true" [ngClass]="{'active': showCourses}">
<ion-icon ios="list-outline" md="list-sharp" />
Kurse
</ion-tab-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,64 @@
.course-content ion-item {
margin-bottom: 16px;
}
.icon {
margin-inline: auto;
aspect-ratio: 1;
width: max-content;
padding: 10px;
border-radius: 50%;
line-height: 20px;
background-color: var(--background);
color: var(--foreground);
}
.courses {
ion-item {
--background: transparent;
--border-color: transparent;
}
.course-container {
display: grid;
gap: 16px;
margin: 16px;
grid-template-columns: repeat(3, 1fr);
ion-card {
margin: 0;
}
ion-item {
justify-content: center;
}
}
}
.timetable {
display: flex;
justify-content: space-evenly;
gap: 5px;
padding-inline: 16px;
div {
flex-grow: 1;
flex-basis: 0;
padding-block: 16px;
}
ion-card {
margin: 10px 0 0;
width: 100%;
ion-card-header, ion-card-content {
padding: 7px;
}
}
}
.hide {
opacity: 0;
}

View File

@@ -0,0 +1,129 @@
import {Component, ElementRef, NgZone, OnInit, ViewChild} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {IonicModule, IonModal} from '@ionic/angular';
import {IServService} from "../../api/iserv.service";
import {Course, Lesson, Timetable} from "../../entities/course";
import {WeekPipe} from "../../pipes/week.pipe";
@Component({
selector: 'app-schedule',
templateUrl: './schedule.page.html',
styleUrls: ['./schedule.page.scss'],
standalone: true,
imports: [IonicModule, CommonModule, FormsModule, WeekPipe]
})
export class SchedulePage implements OnInit {
public showCourses: boolean = false;
public courses: Course[] = [];
public currentCourse: Course;
public timetable: Timetable = {mon: [], tue: [], wed: [], thu: [], fri: []};
public currentLesson: {lesson: Lesson, day: string, time: number};
public rerender: boolean = false;
public colors: {name: string; val: string}[] = [
{name: "Blau", val: "primary"},
{name: "Hellblau", val: "secondary"},
{name: "Lila", val: "tertiary"},
{name: "Grün", val: "success"},
{name: "Gelb", val: "warning"},
{name: "Rot", val: "danger"}
];
@ViewChild('courseModal') courseModal: IonModal;
@ViewChild('tableModal') tableModal: IonModal;
constructor(private iserv: IServService) { }
async ngOnInit() {
if (localStorage.getItem("courses") == undefined) {
const data = await this.iserv.getCoursesAndClass();
if (data.class.startsWith("Q")) {
for (let course of data.courses) {
const short = course.substring(1, 3);
const name = this.iserv.courseNames[short];
if (name == undefined) continue;
this.courses.push({
id: course,
short: short.toUpperCase(),
name: name,
color: this.colors[Math.floor(Math.random() * this.colors.length)].val
});
}
this.courses.sort((a, b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
localStorage.setItem("courses", JSON.stringify(this.courses));
}
}else {
this.courses = JSON.parse(localStorage.getItem("courses"));
}
if (localStorage.getItem("timetable") == undefined) {
for (let day of ['mon', 'tue', 'wed', 'thu', 'fri']) {
for (let i = 0; i < 10; i++) {
this.timetable[day].push(undefined);
}
}
localStorage.setItem("timetable", JSON.stringify(this.timetable));
}else {
this.timetable = JSON.parse(localStorage.getItem("timetable"));
}
}
public async onEditOrAdd(course?: Course, lesson?: {lesson: Lesson, day: string, time: number}) {
this.currentCourse = course;
this.currentLesson = lesson;
if (this.showCourses) await this.courseModal.present();
else await this.tableModal.present();
}
public async updateOrCreateCourse(event: any) {
if (event.detail.role == "delete") {
this.courses.splice(this.courses.indexOf(this.currentCourse), 1);
}
if (event.detail.role == "confirm") {
const data = event.detail.data as Course;
if (this.currentCourse != undefined) {
this.courses[this.courses.indexOf(this.currentCourse)] = data;
delete this.currentCourse;
}else {
this.courses.push(data);
}
}
this.courses.sort((a, b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
localStorage.setItem("courses", JSON.stringify(this.courses));
}
public async updateOrCreateLesson(event: any) {
if (event.detail.role == "delete") {
delete this.timetable[this.currentLesson.day][this.currentLesson.time];
}
if (event.detail.role == "confirm") {
const data = event.detail.data as {lesson: Lesson, day: string, time: number};
this.timetable[data.day][data.time] = data.lesson;
}
localStorage.setItem("timetable", JSON.stringify(this.timetable));
location.reload();
}
public findCourse(id: string): Course {
for (let course of this.courses)
if (course.id == id) return course;
return undefined;
}
}

View File

@@ -14,7 +14,7 @@
margin-left: auto; margin-left: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: end; align-items: flex-end;
.type { .type {
font-size: 18px; font-size: 18px;

View File

@@ -5,6 +5,7 @@ import {AlertController, IonicModule} from '@ionic/angular';
import {UnitsService} from "../../api/units.service"; import {UnitsService} from "../../api/units.service";
import {Substitution, UnitsData} from "../../entities/substitution"; import {Substitution, UnitsData} from "../../entities/substitution";
import {IServService} from "../../api/iserv.service"; import {IServService} from "../../api/iserv.service";
import {Course} from "../../entities/course";
@Component({ @Component({
selector: 'app-substitution', selector: 'app-substitution',
@@ -40,10 +41,31 @@ export class SubstitutionPage implements OnInit {
this.data = await this.units.getSubstitutionPlan("today"); this.data = await this.units.getSubstitutionPlan("today");
const groups = await this.iserv.getGroups(); const data = await this.iserv.getCoursesAndClass();
for (let group of groups) { if (localStorage.getItem("class") == null) {
if (!group.includes(".")) continue; if (!data.class.startsWith("Q")) {
this.courses.push(group.split(".")[1]); this.changeClass(data.class);
}else {
this.changeClass(data.class);
this.showOnlyCourses(true);
this.filterByClasses = true;
}
}
if (data.class.startsWith("Q")) {
if (localStorage.getItem("courses") != undefined) {
const courses = JSON.parse(localStorage.getItem("courses")) as Course[];
for (let course of courses) {
this.courses.push(course.id);
}
}else {
const alert = await this.alerts.create({
header: "Achtung",
message: "Füge deine Kurse im Stundenplan hinzu um sie hier zu filtern!",
buttons: ["Ok"]
});
await alert.present();
}
} }
} }

View File

@@ -0,0 +1,38 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Aufgaben</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Aufgaben</ion-title>
</ion-toolbar>
</ion-header>
<ion-label>Coming soon!</ion-label>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-grid>
<ion-row>
<ion-col>
<ion-tab-button>
<ion-icon ios="list-outline" md="list-sharp" />
Aktuelle Aufgaben
</ion-tab-button>
</ion-col>
<ion-col>
<ion-tab-button>
<ion-icon ios="folder-outline" md="folder-sharp" />
Vergangene Aufgaben
</ion-tab-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,20 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
@Component({
selector: 'app-tasks',
templateUrl: './tasks.page.html',
styleUrls: ['./tasks.page.scss'],
standalone: true,
imports: [IonicModule, CommonModule, FormsModule]
})
export class TasksPage implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@@ -0,0 +1,35 @@
import { Pipe, PipeTransform } from '@angular/core';
import {Lesson} from "../entities/course";
@Pipe({
name: 'week',
standalone: true
})
export class WeekPipe implements PipeTransform {
transform(objects: Lesson[]): Lesson[] {
const week = this.getWeek(new Date()) % 2;
const label = week == 0 ? "a" : "b";
const result = [];
for (let lesson of objects) {
if (lesson != undefined && (lesson.week == "all" || lesson.week == label))
result.push(lesson);
else result.push(undefined);
}
return result;
}
private getWeek(orig: Date): number {
const date = new Date(orig.getTime());
date.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
// January 4 is always in week 1.
const week1 = new Date(date.getFullYear(), 0, 4);
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
- 3 + (week1.getDay() + 6) % 7) / 7);
}
}

View File

@@ -1 +0,0 @@
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB