Skip to content
This repository has been archived by the owner on Oct 4, 2024. It is now read-only.

FE: API, Code Cleanup & Fixes #222

Merged
merged 18 commits into from
Jun 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
VITE_BACKEND_URL=http://localhost:3000/
VITE_BACKEND_URL=http://localhost:3000/
VITE_AUTHOR_NAME_MAX_LENGTH=12
8 changes: 8 additions & 0 deletions frontend/src/components/avatar/Avatar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.avatar {
border-radius: 50%;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
height: 40px !important;
object-fit: cover;
pointer-events: none;
width: 40px !important;
}
12 changes: 12 additions & 0 deletions frontend/src/components/avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import "./Avatar.scss";
import React from "react";

export default function Avatar(props: { userId?: string }) {
return <img className={ "avatar" }
src={ "" }
alt={ "Avatar" }
onError={ ({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping
currentTarget.src = "https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50";
} }/>
}
4 changes: 0 additions & 4 deletions frontend/src/components/button/Button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@
overflow: hidden;
position: relative;

* {
z-index: 10;
}

&::before {
animation: rotate 2s infinite linear;
background-color: transparent;
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from "react";
import "./Button.scss";
import { animateBlob } from "../../def/cool-blobs";

interface Props {
buttonStyle?: "primary" | "glass",
icon?: string,
onClick?: () => Promise<void>,
children?: React.ReactNode,
disabled?: boolean,
placeIconRight?: boolean
placeIconRight?: boolean,
type?: "button" | "reset" | "submit"
}

/**
Expand All @@ -24,8 +26,11 @@ export default function Button(props: Props) {

return (
<button className={ "btn btn-" + (props.buttonStyle ?? "glass") }
type={ props.type }
disabled={ props.disabled || isLoading }
onClick={ async () => {
onClick={ async (e) => {
animateBlob(e);

if (!props.onClick) return;
if (isLoading) return;

Expand All @@ -39,6 +44,8 @@ export default function Button(props: Props) {
<span>{ props.children }</span>
{ props.icon && props.placeIconRight && <i className={ isLoading ? "fi fi-rr-spinner spin" : props.icon }
style={ { marginLeft: "var(--spacing)" } }/> }

<span className={ "button-blob" }/>
</button>
);
}
7 changes: 1 addition & 6 deletions frontend/src/components/dropdown/Dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
border: none;
cursor: pointer;
display: flex;
gap: var(--spacing);
padding: var(--spacing);
width: 100%;

Expand All @@ -113,18 +114,12 @@
}

> i:first-child {
margin-inline-end: var(--spacing);
text-align: center;
width: 20px;
}

> i:last-child {
margin-inline-start: var(--spacing);
}

> span:first-of-type {
flex: 1;
padding-inline-end: calc(var(--spacing) * 2);
}

.shortcut {
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import "./Dropdown.scss";
import { animateBlob } from "../../def/cool-blobs";

interface Item {
label: string,
Expand Down Expand Up @@ -60,7 +61,9 @@ export default function Dropdown(props: {
{ item.header && <div className={ "dropdown-menu-header" }>{ item.header }</div> }

<button className={ "dropdown-menu-item" }
onClick={ () => {
onClick={ (e) => {
animateBlob(e);

setItems(items.map(i => {
return {
...i,
Expand All @@ -79,6 +82,8 @@ export default function Dropdown(props: {

{ (item.items && item.items.length > 0) &&
<i className={ "fi fi-rr-angle-down" + (item.expanded ? " expanded" : "") }/> }

<span className={ "button-blob" }/>
</button>

{ item.expanded && item.items && item.items.length > 0 && renderItems(item.items, level + 1,
Expand Down
68 changes: 68 additions & 0 deletions frontend/src/components/questionpreview/QuestionAnswer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Answer } from "../../def/Question";
import React from "react";
import Avatar from "../avatar/Avatar";
import { useTranslation } from "react-i18next";

/**
* Renders an answer to be displayed in QuestionView
* @param props holds the answer and an index
*/
export default function QuestionAnswer(props: { answer: Answer, index: number }) {
const { t } = useTranslation();

return <div key={ props.index } className={ "container transparent question-answer" }>
<div className={ "question-answer-author" }
style={ {
paddingTop: props.answer.author.type === "ai" ? "var(--spacing)" : 0,
paddingInline: props.answer.author.type === "ai" ? "var(--spacing)" : 0
} }>
{ props.answer.author.type === "ai"
? <i className={ "fi fi-sr-brain" }
style={ {
fontSize: "2em",
height: "auto",
color: "var(--primary-color)",
filter: "drop-shadow(0 0 5px rgba(0, 0, 0, 0.2))"
} }/>
: <Avatar userId={ props.answer.author.id }/> }

<p>
<span>{ props.answer.author.name }</span>
<span className={ "badge" }>{ props.answer.author.type.toUpperCase() }</span>
</p>

<span className={ "caption" }>
{ t('dashboard.questionView.answer.creationDate', { answerCreationDate: props.answer.created }) }
</span>
</div>

<div className={ "question-answer-text" }>
<div className={ "glass" + (props.answer.author.type === "ai" ? " glass-simp" : "") }>
<p>{ props.answer.content }</p>
</div>

<div className={ "question-answer-actions" }>
<div
className={ "question-answer-actions-rate" + (props.answer.opinion === "like" ? " rating" : "") }>
<i className={ "fi fi-rr-social-network primary-icon" } tabIndex={ 0 }/>
<span className={ "question-figure" }>{ props.answer.likes }</span>
<span className={ "question-unit" }>{ t('dashboard.questionView.answer.likes') }</span>
</div>

<div
className={ "question-answer-actions-rate" + (props.answer.opinion === "dislike" ? " rating" : "") }>
<i className={ "fi fi-rr-social-network flipY primary-icon" } tabIndex={ 0 }/>
<span className={ "question-figure" }>{ props.answer.dislikes }</span>
<span className={ "question-unit" }>{ t('dashboard.questionView.answer.dislikes') }</span>
</div>

<div style={ { flex: 1 } }/>

<button className={ "question-report" }>
<i className={ "fi fi-rr-flag" }/>
{ t('dashboard.questionView.answer.report') }
</button>
</div>
</div>
</div>
}
9 changes: 5 additions & 4 deletions frontend/src/components/questionpreview/QuestionPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "./QuestionPreview.scss";
import React from "react";
import { useNavigate } from "react-router-dom";
import { Question } from "../../def/Question";
import Avatar from "../avatar/Avatar";

interface Props {
question: Question;
Expand Down Expand Up @@ -63,15 +64,15 @@ export default function QuestionPreview(props: Props) {
<div className={ "question-details-wrapper" }>
<div className={ "question-stats" }>
<div
className={ "question-stat" + (props.question.rating === "like" ? " rating" : "") }>
className={ "question-stat" + (props.question.opinion === "like" ? " rating" : "") }>
<i className={ "fi fi-rr-social-network primary-icon" }/>
<span
className={ "question-figure" }>{ props.question.likes.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") }</span>
<span className={ "question-unit" }>likes</span>
</div>

<div
className={ "question-stat" + (props.question.rating === "dislike" ? " rating" : "") }>
className={ "question-stat" + (props.question.opinion === "dislike" ? " rating" : "") }>
<i className={ "fi fi-rr-social-network primary-icon flipY" }/>
<span
className={ "question-figure" }>{ props.question.dislikes.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") }</span>
Expand All @@ -96,10 +97,10 @@ export default function QuestionPreview(props: Props) {
</div>

<div className={ "author" }>
<img className={ "avatar" } src={ "https://www.w3schools.com/w3images/avatar2.png" } alt={ "Avatar" }/>
<Avatar userId={ props.question.author.id }/>
<p style={ { margin: 0, display: "flex", flexDirection: "column" } }>
<span className={ "caption" }>Asked by</span>
<span>{ props.question.author.name }</span>
<span>{ props.question.author.name.substring(0, import.meta.env.VITE_AUTHOR_NAME_MAX_LENGTH) }</span>
</p>
</div>
</div>
Expand Down
73 changes: 39 additions & 34 deletions frontend/src/components/questionpreview/QuestionPreviewSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,52 @@ import React from "react";
/**
* Renders a skeleton, based on QuestionPreview
*/
export default function QuestionPreviewSkeleton() {
return <div className={ "container questions-question" } style={ { cursor: "auto" } }>
<div className={ "question" }>
<Skeleton containerClassName={ "tags" }/>

<h2>
<Skeleton width={ 400 }/>
</h2>

<Skeleton containerClassName={ "caption" } width={ 300 }/>
</div>

<div className={ "question-details-wrapper" }>
<div className={ "question-stats" }>
<div className={ "question-stat" }>
<Skeleton width={ 80 } height={ 24 }/>
</div>
export default function QuestionPreviewSkeleton(props: { count?: number }) {
const { count = 1 } = props;
const skeletonArray = Array.from({ length: count });

return skeletonArray.map((_, i) => {
return <div className={ "container questions-question" } style={ { cursor: "auto" } } key={ i }>
<div className={ "question" }>
<Skeleton containerClassName={ "tags" }/>

<div className={ "question-stat" }>
<Skeleton width={ 80 } height={ 24 }/>
</div>
<h2>
<Skeleton width={ 400 }/>
</h2>

<Skeleton containerClassName={ "caption" } width={ 300 }/>
</div>

<div className={ "question-stats" }>
<div className={ "question-stat" }>
<Skeleton width={ 80 } height={ 24 }/>
<div className={ "question-details-wrapper" }>
<div className={ "question-stats" }>
<div className={ "question-stat" }>
<Skeleton width={ 80 } height={ 24 }/>
</div>

<div className={ "question-stat" }>
<Skeleton width={ 80 } height={ 24 }/>
</div>
</div>

<div className={ "question-stat" }>
<Skeleton width={ 80 } height={ 24 }/>
<div className={ "question-stats" }>
<div className={ "question-stat" }>
<Skeleton width={ 80 } height={ 24 }/>
</div>

<div className={ "question-stat" }>
<Skeleton width={ 80 } height={ 24 }/>
</div>
</div>
</div>

<div className={ "author" }>
<Skeleton circle width={ 40 } height={ 40 }/>

<p style={ { margin: 0, display: "flex", flexDirection: "column" } }>
<Skeleton width={ 80 } height={ 12 }/>
<Skeleton width={ 80 } height={ 20 }/>
</p>
<div className={ "author" }>
<Skeleton circle width={ 40 } height={ 40 }/>

<p style={ { margin: 0, display: "flex", flexDirection: "column" } }>
<Skeleton width={ 80 } height={ 12 }/>
<Skeleton width={ 80 } height={ 20 }/>
</p>
</div>
</div>
</div>
</div>
})
}
6 changes: 3 additions & 3 deletions frontend/src/def/Question.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface Question {
tags: string[];
likes: number;
dislikes: number;
rating: "like" | "dislike" | "none";
opinion: "like" | "dislike" | "none";
answers: number;
created: string;
updated: string;
Expand All @@ -19,12 +19,12 @@ export interface Answer {
created: string;
likes: number;
dislikes: number;
rating: "like" | "dislike" | "none";
opinion: "like" | "dislike" | "none";
author: Author;
}

interface Author {
id: string;
name: string;
type: "user" | "pro" | "ai";
type: "guest" | "user" | "pro" | "admin" | "ai";
}
30 changes: 30 additions & 0 deletions frontend/src/def/cool-blobs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";

export const animateBlob = (e: React.MouseEvent) => {
const _blob = e.currentTarget.querySelector("span.button-blob");
if (!_blob) return;
let blob = _blob as HTMLElement;

let buttonRect = e.currentTarget.getBoundingClientRect();
let docEl = document.documentElement;
let left = buttonRect.left + (window.pageXOffset || docEl.scrollLeft || 0);

let x = e.clientX - left;

const fromLeftLimit = 30;
const fromRightLimit = (buttonRect.width - fromLeftLimit);
let fromX = x < fromLeftLimit ? fromLeftLimit : x > fromRightLimit ? fromRightLimit : x;

const toLeftLimit = 60;
const toRightLimit = (buttonRect.width - toLeftLimit);
let toX = x < toLeftLimit ? toLeftLimit : x > toRightLimit ? toRightLimit : x;

if (blob.animate) blob.animate([
{ opacity: 1, width: "60px", height: "80%", top: "10%", transform: "translateX(calc(" + fromX + "px - 50%))" },
{ opacity: 0, width: "120px", height: "100%", top: 0, transform: "translateX(calc(" + toX + "px - 50%))" }
], {
duration: 400,
fill: "forwards",
easing: "ease"
});
}
Loading
Loading