[frontend/components] Add polls to notes and allow voting
This commit is contained in:
parent
70e0d45c12
commit
288c549f20
6 changed files with 195 additions and 8 deletions
|
@ -50,6 +50,10 @@
|
||||||
}
|
}
|
||||||
<MfmText Text="@NoteBase.Text" Emoji="@NoteBase.Emoji"/>
|
<MfmText Text="@NoteBase.Text" Emoji="@NoteBase.Emoji"/>
|
||||||
</span>
|
</span>
|
||||||
|
@if (NoteBase.Poll != null)
|
||||||
|
{
|
||||||
|
<NotePoll Poll="@NoteBase.Poll" Emoji="@NoteBase.Emoji"/>
|
||||||
|
}
|
||||||
@if (NoteBase.Attachments.Count > 0)
|
@if (NoteBase.Attachments.Count > 0)
|
||||||
{
|
{
|
||||||
<NoteAttachments Attachments="NoteBase.Attachments"/>
|
<NoteAttachments Attachments="NoteBase.Attachments"/>
|
||||||
|
@ -144,4 +148,4 @@
|
||||||
{
|
{
|
||||||
Controller?.Unregister(this);
|
Controller?.Unregister(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
92
Iceshrimp.Frontend/Components/Note/NotePoll.razor
Normal file
92
Iceshrimp.Frontend/Components/Note/NotePoll.razor
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
@using Iceshrimp.Frontend.Core.Services
|
||||||
|
@using Iceshrimp.Frontend.Localization
|
||||||
|
@using Iceshrimp.Shared.Schemas.Web
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
|
@inject ApiService Api;
|
||||||
|
@inject IStringLocalizer<Localization> Loc;
|
||||||
|
|
||||||
|
<div class="poll">
|
||||||
|
@if (CanVote())
|
||||||
|
{
|
||||||
|
<div class="poll-options">
|
||||||
|
@foreach (var (choice, i) in Poll.Choices.Select((c, i) => (c, i)))
|
||||||
|
{
|
||||||
|
<span class="poll-option">
|
||||||
|
@if (Poll.Multiple)
|
||||||
|
{
|
||||||
|
<input type="checkbox" @onchange="() => SelectMultiple(i)" id="option-@i" name="poll" checked="@choice.Voted" disabled="@choice.Voted"/>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<input type="radio" @onchange="() => SelectSingle(i)" id="option-@i" name="poll"/>
|
||||||
|
}
|
||||||
|
<label for="option-@i">
|
||||||
|
<MfmText Text="@choice.Value" Emoji="@Emoji" Simple="true"/>
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<button class="btn vote-btn" @onclick="Vote">@Loc["Vote"]</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="poll-results">
|
||||||
|
@{ var total = Poll.Choices.Sum(p => p.Votes); }
|
||||||
|
@foreach (var choice in Poll.Choices)
|
||||||
|
{
|
||||||
|
<span class="poll-result @(choice.Voted ? "voted" : "")" style="--percentage: @(choice.Votes / total * 100)%;">
|
||||||
|
<span class="poll-value">
|
||||||
|
<MfmText Text="@choice.Value" Emoji="@Emoji" Simple="true"/>
|
||||||
|
</span>
|
||||||
|
<span class="poll-info"><span class="vote-count">@Loc["{0} votes", choice.Votes]</span><span class="vote-percentage">@(choice.Votes / total * 100)%</span></span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
List<string> footerText = [];
|
||||||
|
@if (Poll.Multiple)
|
||||||
|
footerText.Add(Loc["Multiple choice"]);
|
||||||
|
@if (Poll.VotersCount != null)
|
||||||
|
footerText.Add(Loc["{0} users voted", Poll.VotersCount]);
|
||||||
|
@if (Poll.ExpiresAt != null)
|
||||||
|
footerText.Add(Poll.ExpiresAt <= DateTime.UtcNow ? Loc["Ended"] : Loc["Ends at {0}", Poll.ExpiresAt.Value.ToLocalTime().ToString("G")]);
|
||||||
|
}
|
||||||
|
<span class="poll-footer">@string.Join(" - ", footerText)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter, EditorRequired] public required NotePollSchema Poll { get; set; }
|
||||||
|
[Parameter, EditorRequired] public required List<EmojiResponse> Emoji { get; set; }
|
||||||
|
private List<int> Choices { get; set; } = [];
|
||||||
|
|
||||||
|
private bool CanVote()
|
||||||
|
{
|
||||||
|
return (Poll.Multiple && !Poll.Choices.All(p => p.Voted) || !Poll.Choices.Any(p => p.Voted)) && (Poll.ExpiresAt == null || Poll.ExpiresAt > DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectMultiple(int choice)
|
||||||
|
{
|
||||||
|
if (Choices.Contains(choice))
|
||||||
|
Choices.RemoveAll(p => p == choice);
|
||||||
|
else
|
||||||
|
Choices.Add(choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectSingle(int choice)
|
||||||
|
{
|
||||||
|
Choices = [choice];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Vote()
|
||||||
|
{
|
||||||
|
if (!CanVote()) return;
|
||||||
|
|
||||||
|
var res = await Api.Notes.AddPollVoteAsync(Poll.NoteId, Choices);
|
||||||
|
if (res != null)
|
||||||
|
{
|
||||||
|
Poll = res;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
85
Iceshrimp.Frontend/Components/Note/NotePoll.razor.css
Normal file
85
Iceshrimp.Frontend/Components/Note/NotePoll.razor.css
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
.poll-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: var(--highlight-color);
|
||||||
|
text-wrap: wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option label:hover {
|
||||||
|
background-color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-option input:checked ~ label {
|
||||||
|
background: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-result {
|
||||||
|
--percentage: 0%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: linear-gradient(to right, var(--notice-color) var(--percentage), var(--highlight-color) var(--percentage), var(--highlight-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-result.voted {
|
||||||
|
background: linear-gradient(to right, var(--accent-primary-color), var(--accent-secondary-color) var(--percentage), var(--highlight-color) var(--percentage), var(--highlight-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-value {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-wrap: wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-info {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-count, .vote-percentage {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-count {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.poll-result {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-count {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,6 +71,10 @@ internal class NoteControllerModel(ApiClient api)
|
||||||
public Task<NoteRefetchResponse?> RefetchNoteAsync(string id) =>
|
public Task<NoteRefetchResponse?> RefetchNoteAsync(string id) =>
|
||||||
api.CallNullableAsync<NoteRefetchResponse>(HttpMethod.Get, $"/notes/{id}/refetch");
|
api.CallNullableAsync<NoteRefetchResponse>(HttpMethod.Get, $"/notes/{id}/refetch");
|
||||||
|
|
||||||
|
public Task<NotePollSchema?> AddPollVoteAsync(string id, List<int> choices) =>
|
||||||
|
api.CallNullableAsync<NotePollSchema>(HttpMethod.Post, $"/notes/{id}/vote",
|
||||||
|
data: new NotePollRequest { Choices = choices });
|
||||||
|
|
||||||
public Task MuteNoteAsync(string id) =>
|
public Task MuteNoteAsync(string id) =>
|
||||||
api.CallAsync(HttpMethod.Post, $"/notes/{id}/mute");
|
api.CallAsync(HttpMethod.Post, $"/notes/{id}/mute");
|
||||||
}
|
}
|
|
@ -17,7 +17,9 @@
|
||||||
--foreground-color: #17151e;
|
--foreground-color: #17151e;
|
||||||
--highlight-color: #2e2b4c;
|
--highlight-color: #2e2b4c;
|
||||||
--hover-color: #3d3a66;
|
--hover-color: #3d3a66;
|
||||||
--accent-color: linear-gradient(to right, #9A92FF, #8372F5);
|
--accent-primary-color: #9A92FF;
|
||||||
|
--accent-secondary-color: #8372F5;
|
||||||
|
--accent-color: linear-gradient(to right, var(--accent-primary-color), var(--accent-secondary-color));
|
||||||
--notice-color: #92c1ff;
|
--notice-color: #92c1ff;
|
||||||
--font-color: #e7edff;
|
--font-color: #e7edff;
|
||||||
--link: #9E9EFF;
|
--link: #9E9EFF;
|
||||||
|
@ -330,4 +332,4 @@ pre:has(code){
|
||||||
--height: 0px;
|
--height: 0px;
|
||||||
min-height: var(--height);
|
min-height: var(--height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,11 +61,11 @@ public class NoteAttachment
|
||||||
|
|
||||||
public class NotePollSchema
|
public class NotePollSchema
|
||||||
{
|
{
|
||||||
[JI] public required string NoteId;
|
public required string NoteId { get; set; }
|
||||||
public required DateTime? ExpiresAt { get; set; }
|
public required DateTime? ExpiresAt { get; set; }
|
||||||
public required bool Multiple { get; set; }
|
public required bool Multiple { get; set; }
|
||||||
public required List<NotePollChoice> Choices { get; set; }
|
public required List<NotePollChoice> Choices { get; set; }
|
||||||
public required int? VotersCount { get; set; }
|
public required int? VotersCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NotePollChoice
|
public class NotePollChoice
|
||||||
|
|
Loading…
Add table
Reference in a new issue