Skip to content
Snippets Groups Projects
Commit 97383364 authored by Simon Künzel's avatar Simon Künzel
Browse files

Add feedback pages

parent d92353ff
No related branches found
No related tags found
1 merge request!12Add feedback pages, Closes #38
Pipeline #6072 passed
......@@ -180,6 +180,26 @@ ui.video_player.login_moodle.not_in_course = "Du bist kein Teilnehmer des Moodle
ui.video_player.login_moodle.refresh_course = "Kurse aktualisieren"
ui.video_player.student_council_only = "Nur für Fachschaftler verfügbar."
ui.feedback.title = "Feedback"
ui.feedback.sent_successfully = "Nachricht wurde erfolgreich gesendet. Vielen Dank!"
ui.feedback.info_text = """
Du möchtest uns etwas mitteilen? Du hast Verbesserungsvorschläge? Du hast einen Fehler gefunden?
<br/>
Dann kannst du uns hier eine Nachricht schreiben. Wir freuen uns über jegliches Feedback. Positiv als auch negativ!
"""
ui.feedback.message = "Nachricht"
ui.feedback.email = "E-Mail (optional)"
ui.feedback.email.regex_invalid_message = "Dies ist keine gültige E-Mail. Die E-Mail darf keine Leerzeichen enthalten"
ui.feedback.consent = """
Ich stimme zu, dass meine Nachricht (und E-Mail, falls angegeben) dauerhaft gespeichert
wird und von der VideoAG verwendet werden darf um Ihre Arbeit zu verbessern. Wenn ich
meine E-Mail angegeben habe, wird diese exklusiv genutzt damit die VideoAG mich
bezüglich meiner Nachricht kontaktieren kann.
"""
ui.feedback.consent.required = "Dies ist notwendig"
ui.feedback.send = "Abschicken"
ui.feedback.error.unable_to_send = "Nachricht konnte nicht gesendet werden"
ui.footer.imprint = "Impressum"
ui.footer.twitter = "X (urspr. Twitter)"
ui.footer.advertisement = "Diese Website wird betrieben von ehrenamtlichen Studierenden der [Fachschaft I/1](https://www.fsmpi.rwth-aachen.de/)."
......
......@@ -184,6 +184,25 @@ ui.video_player.login_moodle.not_in_course = "You are not enrolled in the Moodle
ui.video_player.login_moodle.refresh_course = "Refresh course"
ui.video_player.student_council_only = "Only available to student council members"
ui.feedback.title = "Feedback"
ui.feedback.sent_successfully = "Message was sent successfully. Thank you!"
ui.feedback.info_text = """
You would like to tell us something? You have a suggestion? You found a mistake?
<br/>
Then you can send us a message here. We look forward to any feedback. Positive as well as negative!
"""
ui.feedback.message = "Message"
ui.feedback.email = "E-Mail (optional)"
ui.feedback.email.regex_invalid_message = "This is not a valid E-Mail. The E-Mail may not contain any whitespace"
ui.feedback.consent = """
I agree that my message (and E-Mail, if provided) is stored permanently and may be used by the VideoAG to improve
their work. If I have provided my E-Mail, it will be used exclusively so that the VideoAG can contact me regarding my
message.
"""
ui.feedback.consent.required = "This is required"
ui.feedback.send = "Send"
ui.feedback.error.unable_to_send = "Unable to send message"
ui.footer.imprint = "Imprint"
ui.footer.twitter = "X (formerly Twitter)"
ui.footer.advertisement = "This website is operated by volunteer students of the [student council I/1](https://www.fsmpi.rwth-aachen.de/)."
......
......@@ -17,6 +17,9 @@ import type {
AuthenticationStartOAuthResponse,
AuthenticationStatusRequest,
AuthenticationStatusResponse,
PutFeedbackNewRequest,
GetFeedbackRequest,
GetFeedbackResponse,
GetConfigurationResponse,
GetNewConfigurationResponse,
CreateConfiguredObjectRequest,
......@@ -236,6 +239,32 @@ export class BackendImpl {
);
}
// PUT /feedback/new
putNewFeedback(req: PutFeedbackNewRequest): Promise<{}> {
return this.processResponse<{}>(
this.fetch(`${this.baseUrl()}/feedback/new`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(req),
}),
);
}
// GET /feedback
getFeedback(req: GetFeedbackRequest): Promise<GetFeedbackResponse> {
let query_params = [];
for (const [key, value] of Object.entries(req)) {
if (value !== undefined) {
query_params.push(`${key}=${value}`);
}
}
return this.processResponse<GetFeedbackResponse>(
this.fetch(`${this.baseUrl()}/feedback?${query_params.join("&")}`),
);
}
// GET /object_management/{object_type}/{object_id}/configuration
getOMConfiguration(object_type: string, object_id: int): Promise<GetConfigurationResponse> {
return this.processResponse<GetConfigurationResponse>(
......
......@@ -7,6 +7,9 @@ import type {
AuthenticationStartOAuthResponse,
AuthenticationStatusRequest,
AuthenticationStatusResponse,
PutFeedbackNewRequest,
GetFeedbackRequest,
GetFeedbackResponse,
ChapterSuggestionRequest,
CreateConfiguredObjectRequest,
CreateConfiguredObjectResponse,
......@@ -319,6 +322,14 @@ export class DummyBackend {
return Promise.resolve(this.authenticationStatusResponse);
}
putNewFeedback(req: PutFeedbackNewRequest): Promise<{}> {
return Promise.reject("Not implemented");
}
getFeedback(req: GetFeedbackRequest): Promise<GetFeedbackResponse> {
return Promise.reject("Not implemented");
}
logout(): Promise<void> {
// Return a promise resolving to the mock response
return Promise.resolve();
......
......@@ -120,6 +120,13 @@ export interface media_quality {
priority: int;
}
export interface feedback_entry {
id: int;
time_created: datetime;
email: string;
text: string;
}
export interface api_error {
error_code: string;
message: string;
......@@ -345,6 +352,26 @@ export interface AuthenticationStatusResponse {
user_info?: user_info;
}
/// PUT /feedback/new
// Request Interface
export interface PutFeedbackNewRequest {
email?: string;
text: string;
}
// Response Interface: Not needed as there is no response
/// GET /feedback
// Request Interface
export interface GetFeedbackRequest {
entries_per_page?: int;
page?: int;
}
// Response Interface
export interface GetFeedbackResponse {
page_count: int;
page: feedback_entry[];
}
/// GET /object_management/{object_type}/{object_id}/configuration
// Request Interface
export interface GetConfigurationRequest {
......
......@@ -255,6 +255,11 @@ function UserField({ isUnavailable }: { isUnavailable: boolean }) {
Jobs
</Dropdown.Item>
</li>
<li>
<Dropdown.Item as={Link} href="/internal/feedback">
Feedback
</Dropdown.Item>
</li>
<li className="dropdown-divider" />
<li>
<Dropdown.Item as={Link} href="/internal/user">
......@@ -388,6 +393,13 @@ function NavBar({ status }: { status?: GetStatusResponse }) {
url="/faq"
className="flex-grow-1 text-center"
/>
<NavBarIcon
iconlib="bootstrap"
icon="envelope-fill"
activeIcon="envelope"
url="/feedback"
className="flex-grow-1 text-center"
/>
<a
className={
"nav-link p-2 rounded flex-grow-1 text-center d-flex align-items-center justify-content-center"
......@@ -460,6 +472,17 @@ function NavBar({ status }: { status?: GetStatusResponse }) {
FAQ
</NavBarIcon>
</li>
<li className="nav-item">
<NavBarIcon
iconlib="bootstrap"
icon="envelope-fill"
activeIcon="envelope"
url="/feedback"
className="mx-1"
>
Feedback
</NavBarIcon>
</li>
<a
className={"nav-link p-2 rounded mx-1"}
href={"https://video.fsmpi.rwth-aachen.de"}
......
import type React from "react";
import { useState } from "react";
import Link from "next/link";
import { useBackendContext } from "@/components/BackendProvider";
import { useLanguage } from "@/components/LanguageProvider";
import { showError } from "@/misc/ErrorHandlers";
import { StringEditor, BooleanEditor } from "@/components/TypeEditor";
export default function Feedback() {
const api = useBackendContext();
const { language } = useLanguage();
const [textData, setTextData] = useState<[string, boolean]>(["", false]);
const [emailData, setEmailData] = useState<[string | undefined, boolean]>([undefined, true]);
const [dataConsent, setDataConsent] = useState<boolean>(false);
const [hideErrors, setHideErrors] = useState<boolean>(true);
const [isSending, setIsSending] = useState<boolean>(false);
const [showSuccessfulMessageSent, setShowSuccessfulMessageSent] = useState<boolean>(false);
const sendMessage = () => {
setHideErrors(false);
if (!dataConsent || !textData[1] || !emailData[1]) return;
setIsSending(true);
api.putNewFeedback({
email: emailData[0],
text: textData[0],
})
.then(() => {
setIsSending(false);
setShowSuccessfulMessageSent(true);
})
.catch((err) => {
setIsSending(false);
showError(err, language.get("ui.feedback.error.unable_to_send"));
});
};
if (showSuccessfulMessageSent) {
return (
<>
<h2>{language.get("ui.feedback.title")}</h2>
<div className="alert alert-success">
{language.get("ui.feedback.sent_successfully")}
</div>
<Link href="/" className="btn btn-primary">
<span className="bi bi-chevron-right" />
Zur Startseite
</Link>
</>
);
}
return (
<>
<h2>{language.get("ui.feedback.title")}</h2>
<div>{language.getStyled("ui.feedback.info_text", true)}</div>
<table className="table table-borderless w-100">
<thead>
<tr>
<th className="m-1" />
<th className="w-100" />
</tr>
</thead>
<tbody>
<tr>
<td>{language.get("ui.feedback.message")}</td>
<td>
<StringEditor
value={textData[0]}
updateValue={(newValue, isValid, affectNow) =>
setTextData([newValue, isValid])
}
autoFocus={true}
hideErrors={hideErrors}
minLength={1}
maxLength={16384}
/>
</td>
</tr>
<tr>
<td>{language.get("ui.feedback.email")}</td>
<td>
<StringEditor
value={emailData[0]}
updateValue={(newValue, isValid, affectNow) =>
setEmailData([newValue, isValid])
}
hideErrors={hideErrors}
regex={/^\S+@\S+\.\S+$/}
regexInvalidMessage={language.get(
"ui.feedback.email.regex_invalid_message",
)}
allowUndefined={true}
maxLength={128}
/>
</td>
</tr>
<tr>
<td></td>
<td>
<div className="float-start">
<BooleanEditor
value={dataConsent}
updateValue={(newValue, isValid, affectNow) =>
setDataConsent(newValue)
}
hideErrors={hideErrors}
/>
</div>
<div className="ms-4">
{language.getStyled("ui.feedback.consent", true)}
</div>
{!hideErrors && !dataConsent && (
<div className="ms-4 d-inline invalid-feedback">
{language.get("ui.feedback.consent.required")}
</div>
)}
</td>
</tr>
<tr>
<td></td>
<td>
<button
className="btn btn-primary"
onClick={sendMessage}
disabled={
isSending ||
(!hideErrors && (!dataConsent || !textData[1] || !emailData[1]))
}
>
{language.get("ui.feedback.send")}
</button>
{isSending && (
<div className="spinner-border ms-2 align-middle" role="status" />
)}
</td>
</tr>
</tbody>
</table>
</>
);
}
import { useBackendContext } from "@/components/BackendProvider";
import ModeratorBarrier from "@/components/ModeratorBarrier";
import { ReloadBoundary } from "@/components/ReloadBoundary";
import { useFilteredDataView, PagingNavigation } from "@/components/FilteredData";
import { DateTime } from "luxon";
import { ErrorPage } from "@/misc/ErrorHandlers";
import type { GetFeedbackResponse, int } from "@/api/api_v1_types";
function FeedbackList({
feedback,
setFilter,
}: {
feedback: GetFeedbackResponse;
setFilter: (
key: string,
new_value: any | undefined,
isValid: boolean,
updateNow: boolean,
) => void;
}) {
return (
<>
<div className="table-responsive">
<table className="table table-hover" style={{ tableLayout: "fixed" }}>
<thead>
<tr>
<th scope="col" style={{ width: "11em" }}>
Zeit
</th>
<th scope="col" style={{ width: "15em" }}>
Email
</th>
<th scope="col">Text</th>
</tr>
</thead>
<tbody>
{(feedback?.page ?? []).map((i) => {
const formattedDate = DateTime.fromISO(i.time_created).toFormat(
"yyyy-MM-dd HH:mm:ss",
);
return (
<tr key={i.id}>
<td>{formattedDate}</td>
<td>{i.email ?? ""}</td>
<td>{i.text}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</>
);
}
function FeedbackImpl() {
const api = useBackendContext();
const { filters, setFilter, reloadData, isLoading, data, error } = useFilteredDataView(
(filters) =>
api.getFeedback({
entries_per_page: filters.get("entries_per_page") as int | undefined,
page: filters.get("page") as int | undefined,
}),
);
return (
<div className="card">
<div className="card-header d-flex">Feedback</div>
<div className="card-body">
<p>Hier werden alle gesendeten Feedbacks angezeigt</p>
<ReloadBoundary reloadFunc={reloadData}>
<div className="d-flex">
<PagingNavigation
setFilter={setFilter}
filters={filters}
pageCount={data?.page_count}
/>
{isLoading && <div className="spinner-border ms-3 mt-1" />}
</div>
{error ? (
<ErrorPage error={error} objectName="Feedback" showButtons={false} />
) : (
data && <FeedbackList feedback={data} setFilter={setFilter} />
)}
</ReloadBoundary>
</div>
</div>
);
}
export default function Feedback() {
return (
<ModeratorBarrier>
<FeedbackImpl />
</ModeratorBarrier>
);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment