mirror of https://github.com/Squidex/squidex.git
135 changed files with 2567 additions and 692 deletions
@ -0,0 +1,35 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Apps |
||||
|
{ |
||||
|
public sealed class AppImage |
||||
|
{ |
||||
|
public string MimeType { get; } |
||||
|
|
||||
|
public string Etag { get; } |
||||
|
|
||||
|
public AppImage(string mimeType, string etag = null) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(mimeType, nameof(mimeType)); |
||||
|
|
||||
|
MimeType = mimeType; |
||||
|
|
||||
|
if (string.IsNullOrWhiteSpace(etag)) |
||||
|
{ |
||||
|
Etag = RandomHash.Simple(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Etag = etag; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Commands |
||||
|
{ |
||||
|
public sealed class RemoveAppImage : AppCommand |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Commands |
||||
|
{ |
||||
|
public sealed class UpdateApp : AppCommand |
||||
|
{ |
||||
|
public string Label { get; set; } |
||||
|
|
||||
|
public string Description { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Commands |
||||
|
{ |
||||
|
public sealed class UploadAppImage : AppCommand |
||||
|
{ |
||||
|
public AppImage Image { get; set; } |
||||
|
|
||||
|
public Func<Stream> File { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Apps |
||||
|
{ |
||||
|
[EventType(nameof(AppImageRemoved))] |
||||
|
public sealed class AppImageRemoved : AppEvent |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Apps |
||||
|
{ |
||||
|
[EventType(nameof(AppImageUploaded))] |
||||
|
public sealed class AppImageUploaded : AppEvent |
||||
|
{ |
||||
|
public AppImage Image { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Events.Apps |
||||
|
{ |
||||
|
[EventType(nameof(AppUpdated))] |
||||
|
public sealed class AppUpdated : AppEvent |
||||
|
{ |
||||
|
public string Label { get; set; } |
||||
|
|
||||
|
public string Description { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Entities.Apps.Commands; |
||||
|
using Squidex.Infrastructure.Reflection; |
||||
|
|
||||
|
namespace Squidex.Areas.Api.Controllers.Apps.Models |
||||
|
{ |
||||
|
public sealed class UpdateAppDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The optional label of your app.
|
||||
|
/// </summary>
|
||||
|
public string Label { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The optional description of your app.
|
||||
|
/// </summary>
|
||||
|
public string Description { get; set; } |
||||
|
|
||||
|
public UpdateApp ToCommand() |
||||
|
{ |
||||
|
return SimpleMapper.Map(this, new UpdateApp()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
<sqx-title message="{app} | API | GraphQL" parameter1="app" [value1]="appsState.appName"></sqx-title> |
<sqx-title message="{app} | API | GraphQL" parameter1="app" [value1]="appsState.appDisplayName"></sqx-title> |
||||
|
|
||||
<sqx-panel desiredWidth="*" minWidth="50rem" isFullSize="true"> |
<sqx-panel desiredWidth="*" minWidth="50rem" isFullSize="true"> |
||||
<div inner #graphiQLContainer></div> |
<div inner #graphiQLContainer></div> |
||||
|
|||||
@ -1,6 +1,6 @@ |
|||||
@import '_vars'; |
@import '_vars'; |
||||
@import '_mixins'; |
@import '_mixins'; |
||||
|
|
||||
texarea { |
textarea { |
||||
resize: none; |
resize: none; |
||||
} |
} |
||||
@ -1,29 +1,2 @@ |
|||||
@import '_vars'; |
@import '_vars'; |
||||
@import '_mixins'; |
@import '_mixins'; |
||||
|
|
||||
$circle-size: 2.8rem; |
|
||||
|
|
||||
.backup-status { |
|
||||
& { |
|
||||
@include circle($circle-size); |
|
||||
line-height: $circle-size + .1rem; |
|
||||
text-align: center; |
|
||||
font-size: .4 * $circle-size; |
|
||||
font-weight: normal; |
|
||||
background: $color-border; |
|
||||
color: $color-dark-foreground; |
|
||||
vertical-align: middle; |
|
||||
} |
|
||||
|
|
||||
&-pending { |
|
||||
color: inherit; |
|
||||
} |
|
||||
|
|
||||
&-failed { |
|
||||
background: $color-theme-error; |
|
||||
} |
|
||||
|
|
||||
&-success { |
|
||||
background: $color-theme-green; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,43 @@ |
|||||
|
<form [formGroup]="importForm.form" (ngSubmit)="import()"> |
||||
|
<sqx-modal-dialog (close)="emitClose()"> |
||||
|
<ng-container title> |
||||
|
Import contributors |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container content> |
||||
|
<div class="content"> |
||||
|
<ng-container *ngIf="importStatus; else noStatus"> |
||||
|
<div class="row pb-2" *ngFor="let status of importStatus"> |
||||
|
<div class="col truncate"> |
||||
|
{{status.email}} |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-auto"> |
||||
|
<sqx-status-icon size="sm" |
||||
|
[status]="status.result" |
||||
|
[statusText]="status.resultText"> |
||||
|
</sqx-status-icon> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-template #noStatus> |
||||
|
<textarea class="form-control content" placeholder="user1@squidex.io;user2.squidex.io" formControlName="import"></textarea> |
||||
|
</ng-template> |
||||
|
</div> |
||||
|
|
||||
|
<sqx-form-hint> |
||||
|
<ng-container *ngIf="importForm.numberOfEmails | async; let emails"> |
||||
|
Emails detected: {{emails}} |
||||
|
</ng-container> |
||||
|
|
||||
|
|
||||
|
</sqx-form-hint> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container footer> |
||||
|
<button type="reset" class="float-left btn btn-secondary" (click)="emitClose()">Cancel</button> |
||||
|
<button type="submit" class="float-right btn btn-success" [disabled]="importStatus || (importForm.hasNoUser | async)">Add Contributors</button> |
||||
|
</ng-container> |
||||
|
</sqx-modal-dialog> |
||||
|
</form> |
||||
@ -0,0 +1,10 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
textarea { |
||||
|
resize: none; |
||||
|
} |
||||
|
|
||||
|
.content { |
||||
|
min-height: 300px; |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, EventEmitter, Output } from '@angular/core'; |
||||
|
import { FormBuilder } from '@angular/forms'; |
||||
|
import { empty, of } from 'rxjs'; |
||||
|
import { catchError, mergeMap, tap } from 'rxjs/operators'; |
||||
|
|
||||
|
import { |
||||
|
ContributorsState, |
||||
|
ErrorDto, |
||||
|
ImportContributorsForm |
||||
|
} from '@app/shared'; |
||||
|
|
||||
|
interface ImportStatus { |
||||
|
email: string; |
||||
|
result: 'Pending' | 'Failed' | 'Success'; |
||||
|
resultText: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-import-contributors-dialog', |
||||
|
styleUrls: ['./import-contributors-dialog.component.scss'], |
||||
|
templateUrl: './import-contributors-dialog.component.html' |
||||
|
}) |
||||
|
export class ImportContributorsDialogComponent { |
||||
|
@Output() |
||||
|
public close = new EventEmitter(); |
||||
|
|
||||
|
public importForm = new ImportContributorsForm(this.formBuilder); |
||||
|
public importStatus: ImportStatus[]; |
||||
|
|
||||
|
constructor( |
||||
|
private readonly formBuilder: FormBuilder, |
||||
|
private readonly contributorsState: ContributorsState |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public import() { |
||||
|
const contributors = this.importForm.submit(); |
||||
|
|
||||
|
if (contributors && contributors.length > 0) { |
||||
|
this.importStatus = []; |
||||
|
|
||||
|
for (let contributor of contributors) { |
||||
|
this.importStatus.push({ |
||||
|
email: contributor.contributorId, |
||||
|
result: 'Pending', |
||||
|
resultText: 'Pending' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
of(...contributors).pipe( |
||||
|
mergeMap(c => |
||||
|
this.contributorsState.assign(c, { silent: true }).pipe( |
||||
|
tap(created => { |
||||
|
let status = this.importStatus.find(x => x.email === c.contributorId); |
||||
|
|
||||
|
if (status) { |
||||
|
status.resultText = getSuccess(created); |
||||
|
status.result = 'Success'; |
||||
|
} |
||||
|
}), |
||||
|
catchError((error: ErrorDto) => { |
||||
|
let status = this.importStatus.find(x => x.email === c.contributorId); |
||||
|
|
||||
|
if (status) { |
||||
|
status.resultText = getError(error); |
||||
|
status.result = 'Failed'; |
||||
|
} |
||||
|
|
||||
|
return empty(); |
||||
|
}) |
||||
|
), 1) |
||||
|
).subscribe(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public emitClose() { |
||||
|
this.close.emit(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getError(error: ErrorDto): string { |
||||
|
return error.details[0]; |
||||
|
} |
||||
|
|
||||
|
function getSuccess(created: boolean | undefined): string { |
||||
|
return created ? |
||||
|
'User has been invited and assigned.' : |
||||
|
'User has been assigned'; |
||||
|
} |
||||
@ -1,2 +1,71 @@ |
|||||
@import '_vars'; |
@import '_vars'; |
||||
@import '_mixins'; |
@import '_mixins'; |
||||
|
|
||||
|
.app { |
||||
|
&-image { |
||||
|
position: relative; |
||||
|
min-width: 150px; |
||||
|
min-height: 150px; |
||||
|
} |
||||
|
|
||||
|
&-image-remove { |
||||
|
@include absolute(auto, .5rem, .5rem, auto); |
||||
|
} |
||||
|
|
||||
|
&-progress { |
||||
|
@include absolute(1rem, 1rem, 1rem, 1rem); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@mixin overlay { |
||||
|
& { |
||||
|
@include transition(opacity .4s ease); |
||||
|
@include absolute(0, 0, 0, 0); |
||||
|
@include opacity(0); |
||||
|
@include flex-box; |
||||
|
color: $color-dark-foreground; |
||||
|
} |
||||
|
|
||||
|
&-background { |
||||
|
@include absolute(0, 0, 0, 0); |
||||
|
@include opacity(.7); |
||||
|
background: $color-dark-black; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.upload-button { |
||||
|
& { |
||||
|
margin-top: 1rem; |
||||
|
} |
||||
|
|
||||
|
input { |
||||
|
@include hidden; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.drop-overlay { |
||||
|
& { |
||||
|
@include overlay; |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
&-text { |
||||
|
position: absolute; |
||||
|
font-size: 1.25rem; |
||||
|
font-weight: lighter; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.drag { |
||||
|
.drop-overlay { |
||||
|
@include opacity(1); |
||||
|
} |
||||
|
|
||||
|
.app-image-remove { |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.disabled { |
||||
|
pointer-events: none; |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, Input, OnChanges } from '@angular/core'; |
||||
|
|
||||
|
import { picasso } from '@app/framework/internal'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-avatar', |
||||
|
template: ` |
||||
|
<img |
||||
|
[style.width]="sizeInPx" |
||||
|
[style.height]="sizeInPx" |
||||
|
[attr.src]="imageSource | sqxSafeUrl" |
||||
|
/> |
||||
|
` |
||||
|
}) |
||||
|
export class AvatarComponent implements OnChanges { |
||||
|
public imageSource: string; |
||||
|
public sizeInPx: string; |
||||
|
|
||||
|
@Input() |
||||
|
public identifier: string; |
||||
|
|
||||
|
@Input() |
||||
|
public image: string; |
||||
|
|
||||
|
@Input() |
||||
|
public size = 50; |
||||
|
|
||||
|
public ngOnChanges() { |
||||
|
this.imageSource = this.image || this.createSvg(); |
||||
|
|
||||
|
this.sizeInPx = `${this.size}px`; |
||||
|
} |
||||
|
|
||||
|
private createSvg() { |
||||
|
const svg = picasso(this.identifier); |
||||
|
|
||||
|
return `data:image/svg+xml;utf8,${svg}`; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
<ng-container [ngSwitch]="status"> |
||||
|
<div *ngSwitchCase="'Failed'" class="status status-failed {{size}}" [title]="statusText"> |
||||
|
<i class="icon-exclamation"></i> |
||||
|
</div> |
||||
|
<div *ngSwitchCase="'Success'" class="status status-success {{size}}" [title]="statusText"> |
||||
|
<i class="icon-checkmark"></i> |
||||
|
</div> |
||||
|
<div *ngSwitchCase="'Completed'" class="status status-success {{size}}" [title]="statusText"> |
||||
|
<i class="icon-checkmark"></i> |
||||
|
</div> |
||||
|
<div *ngSwitchDefault class="status status-pending spin {{size}}" [title]="statusText"> |
||||
|
<i class="icon-hour-glass"></i> |
||||
|
</div> |
||||
|
</ng-container> |
||||
@ -0,0 +1,41 @@ |
|||||
|
@import '_mixins'; |
||||
|
@import '_vars'; |
||||
|
|
||||
|
$circle-size-sm: 1.6rem; |
||||
|
|
||||
|
$circle-size-lg: 2.8rem; |
||||
|
|
||||
|
.status { |
||||
|
& { |
||||
|
text-align: center; |
||||
|
background: $color-border; |
||||
|
color: $color-dark-foreground; |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
&.sm { |
||||
|
@include circle($circle-size-sm); |
||||
|
line-height: $circle-size-sm + .1rem; |
||||
|
font-size: .5 * $circle-size-sm; |
||||
|
font-weight: normal; |
||||
|
} |
||||
|
|
||||
|
&.lg { |
||||
|
@include circle($circle-size-lg); |
||||
|
line-height: $circle-size-lg + .1rem; |
||||
|
font-size: .5 * $circle-size-lg; |
||||
|
font-weight: normal; |
||||
|
} |
||||
|
|
||||
|
&-pending { |
||||
|
color: inherit; |
||||
|
} |
||||
|
|
||||
|
&-failed { |
||||
|
background: $color-theme-error; |
||||
|
} |
||||
|
|
||||
|
&-success { |
||||
|
background: $color-theme-green; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-status-icon', |
||||
|
styleUrls: ['./status-icon.component.scss'], |
||||
|
templateUrl: './status-icon.component.html', |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class StatusIconComponent { |
||||
|
@Input() |
||||
|
public status: 'Started' | 'Failed' | 'Success' | 'Completed' | 'Failed' | 'Pending'; |
||||
|
|
||||
|
@Input() |
||||
|
public statusText: string; |
||||
|
|
||||
|
@Input() |
||||
|
public size: 'lg' | 'sm' = 'lg'; |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
import MersenneTwister from 'mersenne-twister'; |
||||
|
|
||||
|
const ALL_COLORS = [ |
||||
|
'rgb(226,27,12)', |
||||
|
'rgb(192,19,78)', |
||||
|
'rgb(125,31,141)', |
||||
|
'rgb(82,46,146)', |
||||
|
'rgb(50,65,145)', |
||||
|
'rgb(11,122,209)', |
||||
|
'rgb(2,135,195)', |
||||
|
'rgb(0,150,170)', |
||||
|
'rgb(0,120,109)', |
||||
|
'rgb(61,140,64)', |
||||
|
'rgb(112,162,54)', |
||||
|
'rgb(174,188,33)', |
||||
|
'rgb(210,157,0)', |
||||
|
'rgb(204,122,0)', |
||||
|
'rgb(231,55,0)' |
||||
|
]; |
||||
|
|
||||
|
const LAYERS = 3; |
||||
|
|
||||
|
const RADIUSES = [20, 25, 30, 35, 40, 45, 50]; |
||||
|
|
||||
|
const X_CENTERS = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; |
||||
|
const Y_CENTERS = [30, 40, 50, 60, 70]; |
||||
|
|
||||
|
function hash(str: string) { |
||||
|
if (str.length === 0) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
let result = 0; |
||||
|
|
||||
|
for (let i = 0; i < str.length; i++) { |
||||
|
result = result * 31 + str.charCodeAt(i); |
||||
|
result = result % (2 ** 32); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
export function picasso(content: string) { |
||||
|
const seed = hash(content); |
||||
|
const rand = new MersenneTwister(seed); |
||||
|
|
||||
|
const colors = [...ALL_COLORS]; |
||||
|
|
||||
|
const generateColor = () => { |
||||
|
const idx = Math.floor(colors.length * rand.random()); |
||||
|
|
||||
|
return colors.splice(idx, 1)[0]; |
||||
|
}; |
||||
|
|
||||
|
const background = `<rect fill="${generateColor()}" width="100" height="100"/>`; |
||||
|
|
||||
|
let shape = ''; |
||||
|
|
||||
|
for (let i = 0; i < LAYERS; i++) { |
||||
|
const rd = RADIUSES[Math.floor(RADIUSES.length * rand.random())]; |
||||
|
|
||||
|
const cx = X_CENTERS[Math.floor(X_CENTERS.length * rand.random())]; |
||||
|
const cy = Y_CENTERS[Math.floor(Y_CENTERS.length * rand.random())]; |
||||
|
|
||||
|
const fill = generateColor(); |
||||
|
|
||||
|
shape += `<circle r="${rd}" cx="${cx}" cy="${cy}" fill="${fill}" opacity="0."/>`; |
||||
|
} |
||||
|
|
||||
|
return `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">${background}${shape}</svg>`; |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue