Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e9cf678
Fix note
eliaspr Jan 24, 2026
0147588
Add endpoint
eliaspr Jan 24, 2026
2a9acc3
save changes
eliaspr Jan 25, 2026
a214000
Merge branch 'main' into eliaspr/309-324-rework-image-UI
eliaspr Jan 25, 2026
7bbe9f2
Merge branch 'main' into eliaspr/309-324-rework-image-UI
eliaspr Jan 31, 2026
6f4cd2c
Add WIP image manager & new route for image upload
eliaspr Jan 31, 2026
088f020
Fix missing assignments
eliaspr Feb 1, 2026
62ef42b
Implement rename button + add delete/rbac buttons
eliaspr Feb 1, 2026
6d6392b
Remove lines that might become unnecessary
eliaspr Feb 1, 2026
3c5d4e5
init; only
eliaspr Feb 1, 2026
d0123e0
Merge branch 'main' into eliaspr/309-324-rework-image-UI
eliaspr Feb 1, 2026
9c50ede
Display number of references & finish images table
eliaspr Feb 1, 2026
af02781
Fix rbac offcanvas for image
eliaspr Feb 1, 2026
9fd44fe
Remove upload functionality from image chooser
eliaspr Feb 1, 2026
607db89
Change icon for new upload button
eliaspr Feb 1, 2026
b020837
Add form for image upload
eliaspr Feb 1, 2026
7e822d9
Replace
eliaspr Feb 1, 2026
df05975
Merge branch 'eliaspr/replace' into eliaspr/309-324-rework-image-UI
eliaspr Feb 1, 2026
ae13962
Remove TODO
eliaspr Feb 1, 2026
bc0a24c
Merge branch 'main' into eliaspr/309-324-rework-image-UI
eliaspr Feb 1, 2026
8c83c6b
Add image alt text
eliaspr Feb 1, 2026
6181e1f
Refactor: Remove image type and rename "FileType" to "FileExtension"
eliaspr Feb 1, 2026
28c678a
Remove image type in frontend
eliaspr Feb 1, 2026
260c1be
Fix example
eliaspr Feb 1, 2026
c8a899a
Implement upload functionality
eliaspr Feb 1, 2026
58dbea1
Add pipe for displaying file size
eliaspr Feb 1, 2026
b279e0c
Display total images size in org
eliaspr Feb 1, 2026
52e0553
Add warning if aspect ratio is too large
eliaspr Feb 1, 2026
059d08a
Configurable image size limit & quality
eliaspr Feb 1, 2026
512ee42
Cleanup & remove "small" class
eliaspr Feb 1, 2026
064450e
Merge branch 'main' into eliaspr/309-324-rework-image-UI
eliaspr Feb 1, 2026
4a2408d
Add new values to docs
eliaspr Feb 1, 2026
d89e033
Fix failing test bc org is not always added
eliaspr Feb 1, 2026
aafe7e0
Fix test
eliaspr Feb 1, 2026
79aef4b
Fix import
eliaspr Feb 1, 2026
8bb9634
Cleanup
eliaspr Feb 1, 2026
8db443a
Simplify
eliaspr Feb 1, 2026
a43a415
Show more info in image chooser
eliaspr Feb 1, 2026
305d1ea
Sorting
eliaspr Feb 1, 2026
ca46dfa
Always show hover info
eliaspr Feb 1, 2026
053ef1b
Fix comment
eliaspr Feb 1, 2026
1d9cee2
Fix colspan
eliaspr Feb 1, 2026
59ec58b
Table layout adjustments
eliaspr Feb 1, 2026
ae0caf1
Trim
eliaspr Feb 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/pages/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ The following environment variables can be set if you want to enable specific fe
| `Turnierplan__LogoUrl` | The URL of the custom logo to be displayed in the header of the public pages. If not specified, the turnierplan.NET logo will be shown instead. | - |
| `Turnierplan__ImprintUrl` | The URL of your external imprint page if you want it to be linked on the public pages. | - |
| `Turnierplan__PrivacyUrl` | The URL of your external privacy page if you want it to be linked on the public pages. | - |
| `Turnierplan__ImageMaxSize` | The maximum allowed file size when uploading an image file. The default value equates to 8 MiB (8 · 1024 · 1024) | `8388608` |
| `Turnierplan__ImageQuality` | Uploaded images are compressed using the `webp` format with the specified quality. A value of `100` will result in lossless compression being uesd. | `80` |

!!! note
The token lifetimes must be specified as .NET `TimeSpan` strings. For example `00:03:00` means 3 minutes or `1.00:00.00` means 1 day.
Expand Down
8 changes: 3 additions & 5 deletions src/Turnierplan.App.Test.Unit/Security/AccessValidatorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void IsActionAllowed___When_Called_With_Indirect_Target___Returns_Expecte
organization.AddRoleAssignment(Role.Contributor, otherPrincipal);

Test(() => new ApiKey(organization, "Test", null, DateTime.MaxValue));
Test(() => new Image(organization, "Test", ImageType.Logo, "", 0, 1, 1));
Test(() => new Image(organization, "Test", "", 0, 1, 1));
Test(() => new Folder(organization, "Test"));
Test(() => new Tournament(organization, "Test", Visibility.Public));
Test(() => new Venue(organization, "Test", ""));
Expand Down Expand Up @@ -121,7 +121,7 @@ public void AddAvailableRoles___When_Called_With_Indirect_Target___Returns_Expec
organization.AddRoleAssignment(Role.Contributor, otherPrincipal);

Test(() => new ApiKey(organization, "Test", null, DateTime.MaxValue));
Test(() => new Image(organization, "Test", ImageType.Logo, "", 0, 1, 1));
Test(() => new Image(organization, "Test", "", 0, 1, 1));
Test(() => new Folder(organization, "Test"));
Test(() => new Tournament(organization, "Test", Visibility.Public));
Test(() => new Venue(organization, "Test", ""));
Expand Down Expand Up @@ -260,12 +260,10 @@ public void AddRolesToResponseHeader___When_Called_Such_That_Entities_Are_Proces
var headers = httpContextAccessor.HttpContext!.Response.Headers;
var headerValues = headers["X-Turnierplan-Roles"];

var organizationId = organization.PublicId.ToString();
var tournamentId1 = tournament1.PublicId.ToString();
var tournamentId2 = tournament2.PublicId.ToString();

headerValues.Should().HaveCount(3);
headerValues.Single(x => x!.StartsWith(organizationId)).Should().Be($"{organizationId}=Reader");
headerValues.Should().HaveCount(2);
headerValues.Single(x => x!.StartsWith(tournamentId1)).Should().Be($"{tournamentId1}=Owner+Reader+Contributor");
headerValues.Single(x => x!.StartsWith(tournamentId2)).Should().Be($"{tournamentId2}=Reader+Contributor");
}
Expand Down
66 changes: 49 additions & 17 deletions src/Turnierplan.App/Client/src/app/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,22 @@ export const de = {
Tournaments: 'Turniere',
Venues: 'Spielstätten',
PlanningRealms: 'Turnierplaner',
Images: 'Bilder',
ApiKeys: 'API-Schlüssel',
Settings: 'Einstellungen'
},
Badges: {
TournamentCount: 'Turniere',
VenueCount: 'Spielstätten',
PlanningRealmCount: 'Turnierplaner',
ImagesCount: 'Bilder',
ImagesTotalSize: 'Gesamtgröße',
ApiKeyCount: 'API-Schlüssel'
},
NewTournament: 'Neues Turnier',
NewVenue: 'Neue Spielstätte',
NewPlanningRealm: 'Neuer Turnierplaner',
UploadImage: 'Bild hochladen',
NewApiKey: 'Neuer API-Schlüssel',
NoTournaments: 'In dieser Organisation gibt es aktuell keine Turniere.\nErstellen Sie ein Turner mit der Schaltfläche oben rechts.',
NoVenues:
Expand Down Expand Up @@ -211,6 +215,26 @@ export const de = {
NoDescription: 'Keine Beschreibung vorhanden',
Open: 'öffnen'
},
Images: {
Preview: 'Vorschau',
Dimensions: '{{w}} x {{h}} px',
CreatedAt: 'Hochgeladen am',
Name: 'Name',
References: {
Header: 'Verwendungen',
Tooltip: 'Gibt an, von wie vielen Turnieren wird dieses Bild verwendet wird',
None: 'keine'
},
NoImages: 'Es sind aktuell keine Bilder vorhanden.',
Rename: {
Title: 'Bild umbenennen',
EnterNewName: 'Geben Sie den neuen Namen für das Bild ein:'
},
DeleteToast: {
Title: 'Bild wurde gelöscht',
Message: 'Das Bild wurde gelöscht.'
}
},
ApiKeys: {
TableLabel: 'API Schlüssel dieser Organisation',
Id: 'ID',
Expand Down Expand Up @@ -598,7 +622,9 @@ export const de = {
}
},
EditImages: {
Title: 'Logos & Bilddateien'
Title: 'Logos & Bilddateien',
BannerAspectRatioWarning:
'Das aktuelle Banner hat eine Auflösung von {{w}} x {{h}} px und ein Seitenverhältnis von {{ratio}}:1. Für eine optimale Darstellung verwenden Sie ein Bild mit einem Seitenverhältnis von mindestens 3:1'
},
MoveToAnotherFolder: {
Title: 'Turnier verschieben',
Expand Down Expand Up @@ -1199,6 +1225,20 @@ export const de = {
}
}
},
UploadImage: {
Title: 'Bild hochladen',
Form: {
File: 'Datei auswählen:',
Name: 'Name:',
NameInvalid: 'Der angegebene Name ist ungültig'
},
NameTooltip: 'Wenn der Name leergelassen wird, wird der Dateinahme der gewählten Datei als Name verwendet.',
Preview: 'Bildvorschau:',
PreviewAlt: 'Bildvorschau der Datei "{{fileName}}"',
OrganizationNotice:
'Es wird ein neues Bild in der Organisation <span class="fw-bold">{{organizationName}}</span> hochgeladen. Das Bild kann anschließend von allen Turnieren und Turnierplanern innerhalb der Organisation verwendet werden.',
Submit: 'Hochladen'
},
CreateApiKey: {
Title: 'Neuen API-Schlüssel erstellen',
Form: {
Expand Down Expand Up @@ -1260,23 +1300,11 @@ export const de = {
Banner: 'Banner'
},
ImageChooser: {
Title: 'Bild hochladen oder auswählen',
Title: 'Bild auswählen',
Remove: 'Bild entfernen',
NoImages: 'Laden Sie Ihr erstes Bild hoch...',
Upload: 'Hochladen',
UploadFailed: 'Das Bild konnte nicht hochgeladen werden. Prüfen Sie die Maße und die maximale Dateigröße.',
Constraints: {
Logo: 'Das Bild muss quadratisch sein mit einer Auflösung zwischen 50x50 und 3000x3000 Pixel. Die maximale Dateigröße beträgt 8 MB.',
Banner:
'Das Bild muss mindestens 50px hoch sein und darf maximal 3000px breit sein. Das Seitenverhältnis muss zwischen 3:1 und 5:1 liegen. Die maximale Dateigröße beträgt 8 MB.'
},
DetailView: {
Title: 'Hier sehen Sie die Detailinformationen zu folgendem Bild:',
Name: 'Dateiname: {{value}}',
CreatedAt: 'Hochgeladen am: {{value}}',
FileSize: 'Dateigröße: {{value}} KB',
Resolution: 'Auflösung: {{width}}x{{height}} px'
}
NoImages: 'In dieser Organisation wurden bisher noch keine Bilder hochgeladen.',
UploadViaOrgPage: 'Neue Bilder können Sie auf der Seite der Organisation hochladen.',
Dimensions: '{{w}} x {{h}} px'
},
MultiSelectFilter: {
All: 'alle',
Expand Down Expand Up @@ -1352,6 +1380,10 @@ export const de = {
Tooltip: 'Ordner',
NotInherited: 'Zuweisung liegt auf diesem Ordner'
},
Image: {
Tooltip: 'Bild',
NotInherited: 'Zuweisung liegt auf diesem Bild'
},
Organization: {
Tooltip: 'Organisation',
NotInherited: 'Zuweisung liegt auf dieser Organisation'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,14 @@
<div class="my-4">
<tp-loading-indicator [marginY]="false" />
</div>
} @else if (isInImageDetailView) {
<div class="px-3 small text-secondary d-flex flex-row align-items-start gap-2">
<i class="bi bi-info-circle align-self-start"></i>
<span class="flex-grow-1" [translate]="'Portal.ImageChooser.DetailView.Title'"></span>
</div>

<div class="px-3 mt-4 d-flex flex-row align-items-center justify-content-evenly">
<img style="max-width: 9em" [src]="imageForDetailView!.url" [alt]="imageForDetailView!.name" [title]="imageForDetailView!.name" />
<div>
<div translate="Portal.ImageChooser.DetailView.Name" [translateParams]="{ value: imageForDetailView!.name }"></div>
<div
translate="Portal.ImageChooser.DetailView.CreatedAt"
[translateParams]="{ value: imageForDetailView!.createdAt | translateDate: 'medium' }"></div>
<div
translate="Portal.ImageChooser.DetailView.FileSize"
[translateParams]="{ value: getRoundedFileSize(imageForDetailView!.fileSize) }"></div>
<div
translate="Portal.ImageChooser.DetailView.Resolution"
[translateParams]="{ width: imageForDetailView!.width, height: imageForDetailView!.height }"></div>
</div>
<div class="d-flex flex-column align-items-center">
<tp-delete-button *tpIsActionAllowed="[imageForDetailView!.id, Actions.GenericWrite]" (confirmed)="deleteCurrentViewedImage()" />
</div>
</div>
} @else {
<div class="px-3 d-flex flex-row align-items-center gap-3">
<div class="flex-grow-1 small text-secondary d-flex flex-row align-items-start gap-2">
<i class="bi bi-info-circle align-self-start"></i>
<span class="flex-grow-1" [translate]="'Portal.ImageChooser.Constraints.' + imageType"></span>
<span class="flex-grow-1" [translate]="'Portal.ImageChooser.UploadViaOrgPage'"></span>
</div>
@if (isUploadingImage) {
<tp-small-spinner />
} @else {
<tp-action-button
*tpIsActionAllowed="[organizationId, Actions.GenericWrite]"
[type]="'outline-primary'"
[icon]="'cloud-upload-fill'"
[title]="'Portal.ImageChooser.Upload'"
(buttonClick)="openFileSelectionDialog()" />
}
</div>

@if (hasUploadError) {
<tp-alert class="px-3 my-3" [type]="'danger'" [icon]="'exclamation-octagon'" [text]="'Portal.ImageChooser.UploadFailed'" />
}

<div class="p-3 d-flex flex-row flex-wrap gap-2 overflow-y-scroll justify-content-start image-container">
@for (image of existingImages; track image.id) {
<div
Expand All @@ -63,24 +25,24 @@
'hover-override': image.id === hoverOverrideImageId
}">
<div
class="hover-buttons d-none position-absolute flex-row align-items-center justify-content-center gap-2"
class="hover-buttons d-none position-absolute flex-column align-items-center justify-content-center"
style="top: 0; left: 0">
<tp-action-button [type]="'primary'" [mode]="'IconOnly'" [icon]="'info-circle'" (buttonClick)="imageForDetailView = image" />
@if (image.id !== currentImageId) {
<tp-action-button
[type]="'success'"
[mode]="'IconOnly'"
[icon]="'check-lg'"
(buttonClick)="image.id !== currentImageId && modal.close({ type: 'ImageSelected', image: image })" />
}
<span class="image-info small text-black text-nowrap tp-text-ellipsis mt-2" [title]="image.name">{{ image.name }}</span>
<span
class="image-info xsmall text-black text-nowrap tp-text-ellipsis"
[translate]="'Portal.ImageChooser.Dimensions'"
[translateParams]="{ w: image.width, h: image.height }"></span>
<span class="image-info xsmall text-black text-nowrap tp-text-ellipsis">{{ image.fileSize | fileSize: 'de' }}</span>
</div>
<div class="image-wrapper d-flex flex-column align-items-center justify-content-center">
<img
style="max-width: 9em"
[src]="image.url"
[alt]="image.name"
[title]="image.name"
(click)="hoverOverrideImageId = image.id" />
<img style="max-width: 9em" [src]="image.url" [alt]="image.name" (click)="hoverOverrideImageId = image.id" />
</div>
</div>
} @empty {
Expand All @@ -91,17 +53,13 @@
</div>
@if (!isLoadingImages) {
<div class="modal-footer">
@if (isInImageDetailView) {
<tp-action-button [type]="'outline-dark'" [title]="'Portal.General.Back'" (buttonClick)="imageForDetailView = undefined" />
} @else {
@if (currentImageId !== undefined && !isInImageDetailView) {
<tp-action-button
[type]="'outline-secondary'"
[title]="'Portal.ImageChooser.Remove'"
(buttonClick)="modal.close({ type: 'ImageSelected', image: undefined })" />
}
<tp-action-button [type]="'outline-dark'" [title]="'Portal.General.Cancel'" (buttonClick)="modal.dismiss()" />
@if (currentImageId !== undefined) {
<tp-action-button
[type]="'outline-secondary'"
[title]="'Portal.ImageChooser.Remove'"
(buttonClick)="modal.close({ type: 'ImageSelected', image: undefined })" />
}
<tp-action-button [type]="'outline-dark'" [title]="'Portal.General.Cancel'" (buttonClick)="modal.dismiss()" />
</div>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
max-height: 40em;
}

.image-info {
max-width: 9em;

&.xsmall {
font-size: 0.7em;
}
}

.image-tile {
&:hover,
&.hover-override {
border-color: #333 !important;

.hover-buttons {
background: rgb(255 255 255 / 60%);
background: rgb(255 255 255 / 85%);
display: flex !important;
}
}
Expand Down
Loading
Loading