chore: remove React/Next.js remnants from Angular codebase

The develop branch had been contaminated by a merge with the old
Next.js/React version of the project. These 66 files (pages, layouts,
_components, _types, server actions, CSS modules) were Next.js App Router
artifacts unused by and invisible to the Angular application.
This commit is contained in:
2026-04-26 17:31:34 +02:00
parent 9ad8129321
commit 86c075db10
70 changed files with 0 additions and 1924 deletions
@@ -1,34 +0,0 @@
import { Pagination, PaginationItem } from "@/modules/common/components/pagination";
import { calcTotalPageNumber } from "@/modules/common/functions/pagination";
import { ArticleCard } from "@/modules/features/article/components/articleCard";
import { fetchFeedArticles } from "@/modules/features/article/fetch/fetchArticleList";
const Pages = ({ totalPages, currentPage }: { totalPages: number; currentPage: number }) => (
<Pagination>
{[...Array(totalPages)].map((_, index) => {
const page = index + 1;
const href = `/?tab=global&page=${page}`;
return (
<PaginationItem href={href} active={page === currentPage} key={index}>
{page}
</PaginationItem>
);
})}
</Pagination>
);
export const FeedArticleList = async ({ currentPage }: { currentPage: number }) => {
const { articles, articlesCount } = await fetchFeedArticles(currentPage);
const totalPages = calcTotalPageNumber(articlesCount, 10);
return articles.length < 1 ? (
<p>No articles found.</p>
) : (
<>
{articles.map((article, index) => (
<ArticleCard key={index} article={article} />
))}
{1 < totalPages && <Pages currentPage={currentPage} totalPages={totalPages} />}
</>
);
};
@@ -1 +0,0 @@
export * from "./feedArticleList";
@@ -1,34 +0,0 @@
import { Pagination, PaginationItem } from "@/modules/common/components/pagination";
import { calcTotalPageNumber } from "@/modules/common/functions/pagination";
import { ArticleCard } from "@/modules/features/article/components/articleCard";
import { fetchGlobalArticles } from "@/modules/features/article/fetch/fetchArticleList";
const Pages = ({ totalPages, currentPage }: { totalPages: number; currentPage: number }) => (
<Pagination>
{[...Array(totalPages)].map((_, index) => {
const page = index + 1;
const href = `/?tab=global&page=${page}`;
return (
<PaginationItem href={href} active={page === currentPage} key={index}>
{page}
</PaginationItem>
);
})}
</Pagination>
);
export const GlobalArticleList = async ({ currentPage }: { currentPage: number }) => {
const { articles, articlesCount } = await fetchGlobalArticles(currentPage);
const totalPages = calcTotalPageNumber(articlesCount, 10);
return articles.length < 1 ? (
<p>No articles found.</p>
) : (
<>
{articles.map((article, index) => (
<ArticleCard key={index} article={article} />
))}
{1 < totalPages && <Pages currentPage={currentPage} totalPages={totalPages} />}
</>
);
};
@@ -1 +0,0 @@
export * from "./globalArticleList";
@@ -1,2 +0,0 @@
export * from "./logoHeader";
export * from "./logoSmall";
@@ -1,44 +0,0 @@
const LogoHeader: React.FC = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="272" height="60" viewBox="0 0 272 60">
<g id="Ad_Astra_3_">
<g>
<polygon fill="#D7C067" points="82.246,32.325 80.22,24.75 67.628,24.75 65.604,32.325 " />
<polygon fill="#D7C067" points="78.265,47.726 79.182,51.349 87.329,51.349 82.955,35.167 74.905,35.167 76.599,41.509 71.25,41.509 72.944,35.166 64.795,35.167 60.519,51.349 68.62,51.349 69.587,47.726 " />
</g>
<g>
<path fill="#D7C067" d="M115.256,32.325v-1.523c0-1.663-0.637-3.104-1.895-4.28c-1.258-1.176-2.797-1.773-4.575-1.773H89.241 v7.576H115.256z" />
<path fill="#D7C067" d="M115.256,35.167h-8.1v8.606h-9.814v-8.606h-8.101v16.182h19.544c1.779,0,3.318-0.596,4.575-1.772 c1.258-1.177,1.895-2.617,1.895-4.28V35.167z" />
</g>
<g>
<polygon fill="#D7C067" points="149.168,32.325 147.142,24.75 134.552,24.75 132.527,32.325 " />
<polygon fill="#D7C067" points="145.187,47.726 146.104,51.349 154.25,51.349 149.878,35.167 141.827,35.167 143.522,41.509 138.171,41.509 139.866,35.166 131.718,35.167 127.441,51.349 135.542,51.349 136.51,47.726 " />
</g>
<g>
<path fill="#D7C067" d="M182.177,41.219c0-1.662-0.637-3.103-1.894-4.279c-1.257-1.177-2.797-1.773-4.576-1.773h-19.537 c0.07,1.543,0.704,2.885,1.888,3.992c1.257,1.175,2.797,1.771,4.576,1.771h11.444v2.843h-17.915v7.576h19.543 c1.779,0,3.319-0.596,4.576-1.772c1.256-1.177,1.894-2.617,1.894-4.28V41.219z" />
<path fill="#D7C067" d="M156.164,32.325h26.013V24.75h-19.542c-1.778,0-3.319,0.597-4.576,1.773 c-1.257,1.175-1.895,2.617-1.895,4.28V32.325z" />
</g>
<g>
<rect x="194.171" y="35.167" fill="#D7C067" width="8.101" height="16.182" />
<rect x="185.214" y="24.75" fill="#D7C067" width="26.015" height="7.576" />
</g>
<g>
<path fill="#D7C067" d="M240.278,32.325v-1.523c0-1.663-0.637-3.104-1.895-4.28c-1.259-1.176-2.798-1.773-4.576-1.773h-19.544 v7.576H240.278z" />
<path fill="#D7C067" d="M214.264,51.349h8.1V40.93h9.815v10.418h8.1V41.219c0-0.852-0.604-1.85-1.796-2.966l-0.219-0.204 l0.219-0.205c1.061-0.992,1.655-1.891,1.774-2.677h-25.992V51.349z" />
</g>
<g>
<polygon fill="#D7C067" points="259.936,47.726 260.854,51.349 269,51.349 264.627,35.167 256.577,35.167 258.272,41.509 252.92,41.509 254.616,35.166 246.466,35.167 242.189,51.349 250.291,51.349 251.26,47.726 " />
<polygon fill="#D7C067" points="263.916,32.325 261.893,24.75 249.301,24.75 247.275,32.325 " />
</g>
</g>
<g id="Star_1_">
<path fillRule="evenodd" clipRule="evenodd" fill="#DDDCDB" d="M83.315,18.245c-18.977-0.521-37.805,14.08-53.743,25.012 l6.676-20.473l2.715,7.387c0.54,1.35,1.15,1.016,2.676,0.347C56.554,23.297,63.892,15.637,83.315,18.245z" />
<path fill="#D7C067" d="M25.725,48.51l10.771-8.175l11.39,11.317l-11.007-6.955L19.927,57l5.516-21.621L3,23.726l27.347,8.99 L25.725,48.51z" />
<path fill="#D7C067" d="M31.53,28.94L8.308,22.501l20.681,1.383l3.331-10.934l2.55,6.38L31.53,28.94z" />
<path fill="#D7C067" d="M35.352,3l8.294,19.697c0.14,0.322,0.265,0.545,0.554,0.408C56.45,16.93,66.625,7.79,96.508,17.836 c-31.457-6.516-38.135,2.598-55.074,10.246c-0.337,0.14-0.624-0.113-0.914-0.832L33.412,9.368L35.352,3z" />
</g>
</svg>
);
};
export default LogoHeader;
@@ -1,44 +0,0 @@
const LogoSmall: React.FC = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="218" height="48" viewBox="0 0 218 48">
<g id="Ad_Astra_3_">
<g>
<polygon fill="#D7C067" points="66.158,26.352 64.543,20.319 54.508,20.319 52.896,26.352 " />
<polygon fill="#D7C067" points="62.985,38.615 63.716,41.5 70.209,41.5 66.724,28.614 60.308,28.614 61.658,33.665 57.394,33.665 58.745,28.614 52.25,28.614 48.842,41.5 55.299,41.5 56.069,38.615 " />
</g>
<g>
<path fill="#D7C067" d="M92.467,26.352v-1.212c0-1.325-0.508-2.472-1.51-3.409c-1.003-0.937-2.23-1.412-3.647-1.412H71.733v6.033 H92.467z" />
<path fill="#D7C067" d="M92.467,28.614h-6.456v6.853H78.19v-6.853h-6.457V41.5H87.31c1.418,0,2.645-0.475,3.647-1.411 c1.002-0.938,1.51-2.083,1.51-3.409V28.614z" />
</g>
<g>
<polygon fill="#D7C067" points="119.495,26.352 117.88,20.319 107.846,20.319 106.232,26.352 " />
<polygon fill="#D7C067" points="116.322,38.615 117.053,41.5 123.546,41.5 120.061,28.614 113.644,28.614 114.995,33.665 110.73,33.665 112.081,28.614 105.588,28.614 102.179,41.5 108.635,41.5 109.406,38.615 " />
</g>
<g>
<path fill="#D7C067" d="M145.803,33.434c0-1.324-0.508-2.471-1.509-3.408c-1.002-0.937-2.229-1.412-3.647-1.412h-15.571 c0.056,1.229,0.561,2.297,1.505,3.179c1.001,0.936,2.229,1.41,3.647,1.41h9.121v2.264H125.07V41.5h15.576 c1.418,0,2.645-0.475,3.647-1.411c1.001-0.938,1.509-2.083,1.509-3.409V33.434z" />
<path fill="#D7C067" d="M125.07,26.352h20.732v-6.033h-15.575c-1.417,0-2.646,0.475-3.647,1.412 c-1.002,0.936-1.51,2.084-1.51,3.409V26.352z" />
</g>
<g>
<rect x="155.362" y="28.614" fill="#D7C067" width="6.456" height="12.886" />
<rect x="148.223" y="20.319" fill="#D7C067" width="20.733" height="6.033" />
</g>
<g>
<path fill="#D7C067" d="M192.109,26.352v-1.212c0-1.325-0.508-2.472-1.51-3.409c-1.003-0.937-2.23-1.412-3.647-1.412h-15.576 v6.033H192.109z" />
<path fill="#D7C067" d="M171.375,41.5h6.456v-8.296h7.823V41.5h6.456v-8.066c0-0.679-0.481-1.473-1.432-2.362l-0.174-0.163 l0.174-0.163c0.846-0.79,1.319-1.506,1.414-2.132h-20.716V41.5z" />
</g>
<g>
<polygon fill="#D7C067" points="207.776,38.615 208.508,41.5 215,41.5 211.515,28.614 205.099,28.614 206.45,33.665 202.185,33.665 203.536,28.614 197.041,28.614 193.632,41.5 200.088,41.5 200.861,38.615 " />
<polygon fill="#D7C067" points="210.948,26.352 209.335,20.319 199.3,20.319 197.686,26.352 " />
</g>
</g>
<g id="Star_1_">
<path fillRule="evenodd" clipRule="evenodd" fill="#DDDCDB" d="M67.011,15.14c-15.125-0.415-30.13,11.212-42.833,19.917 l5.321-16.303l2.163,5.883c0.43,1.075,0.917,0.809,2.133,0.276C45.682,19.162,51.53,13.062,67.011,15.14z" />
<path fill="#D7C067" d="M21.111,39.24l8.584-6.51l9.078,9.012l-8.772-5.538L16.491,46l4.396-17.216L3,19.504l21.795,7.159 L21.111,39.24z" />
<path fill="#D7C067" d="M25.738,23.656L7.23,18.529l16.482,1.101l2.655-8.707l2.032,5.081L25.738,23.656z" />
<path fill="#D7C067" d="M28.784,3l6.61,15.685c0.112,0.257,0.211,0.435,0.442,0.326c9.763-4.918,17.872-12.196,41.689-4.196 c-25.071-5.188-30.394,2.068-43.894,8.159c-0.269,0.111-0.497-0.09-0.729-0.662L27.238,8.07L28.784,3z" />
</g>
</svg>
);
};
export default LogoSmall;
@@ -1 +0,0 @@
export * from "./tagArticleList";
@@ -1,34 +0,0 @@
import { Pagination, PaginationItem } from "@/modules/common/components/pagination";
import { calcTotalPageNumber } from "@/modules/common/functions/pagination";
import { ArticleCard } from "@/modules/features/article/components/articleCard";
import { fetchArticlesByTag } from "@/modules/features/article/fetch/fetchArticleList";
const Pages = ({ totalPages, currentPage }: { totalPages: number; currentPage: number }) => (
<Pagination>
{[...Array(totalPages)].map((_, index) => {
const page = index + 1;
const href = `/?tab=global&page=${page}`;
return (
<PaginationItem href={href} active={page === currentPage} key={index}>
{page}
</PaginationItem>
);
})}
</Pagination>
);
export const TagArticleList = async ({ tag, currentPage }: { tag: string; currentPage: number }) => {
const { articles, articlesCount } = await fetchArticlesByTag(tag, currentPage);
const totalPages = calcTotalPageNumber(articlesCount, 10);
return articles.length < 1 ? (
<p>No articles found.</p>
) : (
<>
{articles.map((article, index) => (
<ArticleCard key={index} article={article} />
))}
{1 < totalPages && <Pages currentPage={currentPage} totalPages={totalPages} />}
</>
);
};
@@ -1,7 +0,0 @@
import { fetchTagsList } from "@/modules/features/article/fetch/fetchTagsList";
import { TagList as TagListPresentation } from "./presentation";
export const TagList = async () => {
const tags = await fetchTagsList();
return <TagListPresentation tags={tags} />;
};
-1
View File
@@ -1 +0,0 @@
export * from "./container";
@@ -1,16 +0,0 @@
import { Tag } from "@/modules/features/article/components/tag";
type Props = { tags: string[] };
export const TagList = ({ tags }: Props) => (
<div className="sidebar">
<p>Popular Tags</p>
<div className="tag-list">
{tags.map((tag, index) => (
<Tag component="a" variant="filled" href={`/?tab=tag&tag=${tag}`} key={index}>
{tag}
</Tag>
))}
</div>
</div>
);
-1
View File
@@ -1 +0,0 @@
export * from "./timeClock";
-184
View File
@@ -1,184 +0,0 @@
"use client";
import React, { useEffect } from "react";
const TimeClock: React.FC = () => {
useEffect(() => {
const numbers = [
[1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1], // 0
[1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1], // 1
[1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1], // 2
[1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1], // 3
[1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1], // 4
[1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1], // 5
[1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1], // 6
[1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0], // 7
[1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1], // 8
[1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1], // 9
];
const blocks: Array<Element[]> = [];
const digits = Array.from(document.querySelectorAll(".block"));
for (let i = 0; i < 4; i++) {
blocks.push(digits.slice(i * 15, i * 15 + 15));
}
const setNum = (block: Array<Element>, num: number) => {
const n = numbers[num];
for (let i = 0; i < block.length; i++) {
block[i].classList[n[i] === 1 ? "add" : "remove"]("active");
}
};
const time = {
s: "",
m: "",
h: "",
p: 0,
};
// time loop
const animator = () => {
const d = new Date();
let h = d.getHours().toString(),
m = d.getMinutes().toString(),
s = d.getSeconds().toString();
s = s.length === 1 ? "0" + s : s;
m = m.length === 1 ? "0" + m : m;
h = h.length === 1 ? "0" + h : h;
if (s !== time.s) {
for (let i = 0; i < digits.length; i++) {
const d = digits[i];
if (i === +s) {
d.classList.add("second");
if (time.p !== null) digits[time.p].classList.remove("second");
time.p = i;
time.s = s;
}
}
}
if (m !== time.m) {
setNum(blocks[2], parseInt(Array.from(m)[0]));
setNum(blocks[3], parseInt(Array.from(m)[1]));
time.m = m;
}
if (h !== time.h) {
setNum(blocks[0], parseInt(Array.from(h)[0]));
setNum(blocks[1], parseInt(Array.from(h)[1]));
time.h = h;
}
window.requestAnimationFrame(animator);
};
// init
window.requestAnimationFrame(animator);
// toggle button
const d = new Date();
const days = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
const months = [
"Janvier",
"Février",
"Mars",
"Avril",
"Mai",
"Juin",
"Juillet",
"Août",
"Septembre",
"Octobre",
"Novembre",
"Décembre",
];
const lib_d = days[d.getDay()];
const lib_m = months[d.getMonth()];
const lib_n = d.getDate();
const lib_y = d.getFullYear();
document.getElementById("datelib")!.innerHTML = lib_d + " " + lib_n + " " + lib_m + " " + lib_y;
}, []);
return (
<>
<div className="date d-flex mb-2 pt-2">
<div className="flex-grow-1"></div>
<div className="datelib mt-2 pt-1 ps-1 font-monospace fs-5">
<span className="icon-calendar me-2" aria-hidden="true"></span>
<span id="datelib"></span>
</div>
</div>
<div className="time d-flex">
<div className="flex-grow-1"></div>
<div className="clock position-relative d-flex flex-column flex-wrap">
<div className="block" data-num="0"></div>
<div className="block" data-num="1"></div>
<div className="block" data-num="2"></div>
<div className="block" data-num="3"></div>
<div className="block" data-num="4"></div>
<div className="block" data-num="5"></div>
<div className="block" data-num="6"></div>
<div className="block" data-num="7"></div>
<div className="block" data-num="8"></div>
<div className="block" data-num="9"></div>
<div className="block" data-num="10"></div>
<div className="block" data-num="11"></div>
<div className="block" data-num="12"></div>
<div className="block" data-num="13"></div>
<div className="block" data-num="14"></div>
<div className="block" data-num="15"></div>
<div className="block" data-num="16"></div>
<div className="block" data-num="17"></div>
<div className="block" data-num="18"></div>
<div className="block" data-num="19"></div>
<div className="block" data-num="20"></div>
<div className="block" data-num="21"></div>
<div className="block" data-num="22"></div>
<div className="block" data-num="23"></div>
<div className="block" data-num="24"></div>
<div className="block" data-num="25"></div>
<div className="block" data-num="26"></div>
<div className="block" data-num="27"></div>
<div className="block" data-num="28"></div>
<div className="block" data-num="29"></div>
<div className="block" data-num="30"></div>
<div className="block" data-num="31"></div>
<div className="block" data-num="32"></div>
<div className="block" data-num="33"></div>
<div className="block" data-num="34"></div>
<div className="block" data-num="35"></div>
<div className="block" data-num="36"></div>
<div className="block" data-num="37"></div>
<div className="block" data-num="38"></div>
<div className="block" data-num="39"></div>
<div className="block" data-num="40"></div>
<div className="block" data-num="41"></div>
<div className="block" data-num="42"></div>
<div className="block" data-num="43"></div>
<div className="block" data-num="44"></div>
<div className="block" data-num="45"></div>
<div className="block" data-num="46"></div>
<div className="block" data-num="47"></div>
<div className="block" data-num="48"></div>
<div className="block" data-num="49"></div>
<div className="block" data-num="50"></div>
<div className="block" data-num="51"></div>
<div className="block" data-num="52"></div>
<div className="block" data-num="53"></div>
<div className="block" data-num="54"></div>
<div className="block" data-num="55"></div>
<div className="block" data-num="56"></div>
<div className="block" data-num="57"></div>
<div className="block" data-num="58"></div>
<div className="block" data-num="59"></div>
<div className="divider position-absolute"></div>
</div>
</div>
</>
);
};
export default TimeClock;
-9
View File
@@ -1,9 +0,0 @@
import { z } from "zod";
export const SearchParams = z.object({
page: z.coerce.number().int().min(1).catch(1),
tab: z.enum(["yours", "global", "tag"]).default("global"),
tag: z.string().optional(),
});
export type SearchParams = z.infer<typeof SearchParams>;
@@ -1,9 +0,0 @@
.actions {
& * + * {
margin-left: 10px;
}
}
.action-btn + .action-btn {
margin-left: 5px;
}
@@ -1,80 +0,0 @@
import { Container, Col, Row } from "react-bootstrap";
import { Button } from "@/modules/common/components/button";
import { DeleteArticleButton } from "@/modules/features/article/components/deleteArticleButton";
import { FavoriteButton } from "@/modules/features/article/components/favoriteButton";
import { Tag } from "@/modules/features/article/components/tag";
import { fetchArticle } from "@/modules/features/article/fetch/fetchArticle";
import { convertMarkdownToHtml } from "@/modules/features/article/functions";
import { FollowButton } from "@/modules/features/profile/components/followButton";
import { Article, User } from "@/utils/types/models";
import Link from "next/link";
import Image from "next/image";
import { ReactNode } from "react";
import styles from "./articleArea.module.css";
import { showDeleteArticleButton, showEditArticleButton, showFollowButton } from "./functions";
import { getSession } from "@/utils/auth/session";
import { fetchCurrentUser } from "@/modules/features/auth/fetch/fetchCurrentUser";
const Actions = ({ article, currentUser }: { article: Article; currentUser?: User }) => {
const profile = article.author;
return (
<div className="article-meta">
<Link href={`/profile/${profile.username}`}>{profile.image && <Image src={profile.image} alt="" />}</Link>
<div className="info">
<Link href={`/profile/${profile.username}`} className="author">
{profile.username}
</Link>
<span className="date">{article.createdAt.toDateString()}</span>
</div>
{showFollowButton(profile.username, currentUser) && (
<FollowButton {...profile} className={styles["action-btn"]} />
)}
<FavoriteButton {...article} showMessage={true} className={styles["action-btn"]} />
{showEditArticleButton(profile.username, currentUser) && (
<Button component="a" href={`/editor/${article.slug}`} color="secondary" className={styles["action-btn"]}>
<i className="ion-edit"></i> Edit Article
</Button>
)}
{showDeleteArticleButton(profile.username, currentUser) && (
<DeleteArticleButton slug={article.slug} className={styles["action-btn"]} />
)}
</div>
);
};
export const ArticleArea = async ({ slug, children }: { slug: string; children: ReactNode }) => {
const article = await fetchArticle(slug);
const body = await convertMarkdownToHtml(article.body);
const currentUser = (await getSession()) ? await fetchCurrentUser() : undefined;
return (
<section className="article-page">
<Container className="banner">
<h1>{article.title}</h1>
<Actions article={article} currentUser={currentUser} />
</Container>
<Container className="page">
<Row className="article-content">
<Col xs={12}>
<div dangerouslySetInnerHTML={{ __html: body }} />
<ul className="list-unstyled list-inline">
{article.tagList.map((tag, index) => (
<li className="list-inline-item me-2" key={index}>
<Tag component="badge" color="success">
{tag}
</Tag>
</li>
))}
</ul>
</Col>
</Row>
<hr />
<div className="article-actions">
<Actions article={article} currentUser={currentUser} />
</div>
{children}
</Container>
</section>
);
};
@@ -1,13 +0,0 @@
import { User } from "@/utils/types/models";
export const showFollowButton = (authorUsername: string, currentUser: User | undefined) => {
return authorUsername !== currentUser?.username;
};
export const showEditArticleButton = (authorUsername: string, currentUser: User | undefined) => {
return authorUsername === currentUser?.username;
};
export const showDeleteArticleButton = (authorUsername: string, currentUser: User | undefined) => {
return authorUsername === currentUser?.username;
};
@@ -1 +0,0 @@
export * from "./articleArea";
@@ -1,30 +0,0 @@
"use server";
import { createApiClient } from "@/utils/api/apiClient";
import { getSession } from "@/utils/auth/session";
import { revalidateTag } from "next/cache";
import { redirect } from "next/navigation";
import { Inputs } from "./types";
export const deleteCommentAction = async (_prevState: undefined, inputs: Inputs): Promise<undefined> => {
if ((await getSession()) == null) {
redirect("/login");
}
const apiClient = createApiClient({
path: "/articles/{slug}/comments/{id}",
method: "delete",
params: {
path: inputs,
},
});
const response = await apiClient.sendRequest();
if (response.result === "success") {
revalidateTag("");
return undefined;
}
throw new Error("api error");
};
@@ -1,36 +0,0 @@
"use client";
import { Comment, User } from "@/utils/types/models";
import { use, useActionState } from "react";
import { deleteCommentAction as serverAction } from "./action";
import { CommentCard as CommentCardPresentation } from "./presentation";
import { showDeleteCommentButton } from "./functions";
type Props = {
slug: string;
comment: Comment;
currentUserPromise?: Promise<User>;
};
export const CommentCard = ({ slug, comment, currentUserPromise }: Props) => {
const [_state, dispatch, isPending] = useActionState(serverAction, undefined);
const action = () => {
if (!confirm("Delete comment?")) {
return;
}
dispatch({ slug, id: comment.id });
};
return (
<CommentCardPresentation
comment={comment}
showDeleteCommentButton={showDeleteCommentButton(
comment.author.username,
currentUserPromise && use(currentUserPromise),
)}
deleteCommentAction={action}
isPending={isPending}
/>
);
};
@@ -1,5 +0,0 @@
import { User } from "@/utils/types/models";
export const showDeleteCommentButton = (commentAuthorUsername: string, currentUser?: User) => {
return commentAuthorUsername === currentUser?.username;
};
@@ -1 +0,0 @@
export * from "./container";
@@ -1,13 +0,0 @@
.form {
display: inline;
& button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
}
}
@@ -1,39 +0,0 @@
import { Button, Form } from "react-bootstrap";
import Link from "next/link";
import Image from "next/image";
import { Comment } from "@/utils/types/models";
import styles from "./presentation.module.css";
type Props = {
comment: Comment;
showDeleteCommentButton?: boolean;
deleteCommentAction?: () => void;
isPending?: boolean;
};
export const CommentCard = ({ comment, showDeleteCommentButton, deleteCommentAction }: Props) => {
return (
<div className="card">
<div className="card-block">
<p className="card-text">{comment.body}</p>
</div>
<div className="card-footer">
<Link href={`/profile/${comment.author.username}`} className="comment-author">
{comment.author.image && <Image src={comment.author.image} className="comment-author-img" alt="" />}
</Link>
&nbsp;
<Link href={`/profile/${comment.author.username}`} className="comment-author">
{comment.author.username}
</Link>
<span className="date-posted">{comment.createdAt.toDateString()}</span>
{showDeleteCommentButton && (
<Form action={deleteCommentAction} className={styles["form"]}>
<Button className="mod-options" variant="primary" type="submit">
<i className="icon-trash" />
</Button>
</Form>
)}
</div>
</div>
);
};
@@ -1,4 +0,0 @@
export type Inputs = {
slug: string;
id: string;
};
@@ -1,57 +0,0 @@
"use server";
import { createApiClient } from "@/utils/api/apiClient";
import { getSession } from "@/utils/auth/session";
import { SubmissionResult } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { revalidateTag } from "next/cache";
import { redirect } from "next/navigation";
import { inputsSchema } from "./types";
export const postCommentAction = async (
_prevState: unknown,
formData: FormData,
): Promise<SubmissionResult<string[]>> => {
if ((await getSession()) == null) {
redirect("/login");
}
const submission = parseWithZod(formData, { schema: inputsSchema });
if (submission.status !== "success") {
return submission.reply();
}
const { slug, body } = submission.value;
const apiClient = createApiClient({
path: "/articles/{slug}/comments",
method: "post",
params: {
path: {
slug,
},
body: {
comment: {
body,
},
},
},
});
const response = await apiClient.sendRequest();
if (response.result === "success") {
revalidateTag("");
return submission.reply();
}
switch (response.statusCode) {
case 422:
return submission.reply({
formErrors: Object.values(response.error.errors).flat(),
});
default:
throw new Error("api error");
}
};
@@ -1,26 +0,0 @@
"use client";
import { User } from "@/utils/types/models";
import { use, useActionState } from "react";
import { postCommentAction } from "./action";
import { CommentForm as CommentFormPresentation } from "./presentation";
type Props = {
slug: string;
currentUserPromise?: Promise<User>;
};
export const CommentForm = ({ slug, currentUserPromise }: Props) => {
const currentUser = currentUserPromise && use(currentUserPromise);
const [state, action, isPending] = useActionState(postCommentAction, undefined);
return (
<CommentFormPresentation
slug={slug}
authorImage={currentUser?.image}
result={state}
postCommentAction={action}
isPending={isPending}
/>
);
};
@@ -1 +0,0 @@
export * from "./container";
@@ -1,68 +0,0 @@
import { Button, Form } from "react-bootstrap";
import Image from "next/image";
import { ErrorMessage } from "@/modules/common/components/errorMessage";
import { SubmissionResult, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { startTransition } from "react";
import { inputsSchema } from "./types";
type Props = {
slug?: string;
authorImage?: string;
result?: SubmissionResult<string[]>;
postCommentAction?: (formData: FormData) => void;
isPending?: boolean;
};
export const CommentForm = ({ slug, authorImage, result, postCommentAction, isPending }: Props) => {
const [form, fields] = useForm({
defaultValue: {
slug,
},
lastResult: result,
onValidate({ formData }) {
return parseWithZod(formData, { schema: inputsSchema });
},
// onSubmit is defined to avoid resetting form after successful submission
// see https://github.com/edmundhung/conform/discussions/606
onSubmit(event, { formData }) {
event.preventDefault();
startTransition(() => {
postCommentAction?.(formData);
});
},
shouldRevalidate: "onBlur",
});
const errors = Object.values(form.allErrors).flat();
return (
<>
<Form className="card comment-form" id={form.id} action={postCommentAction} onSubmit={form.onSubmit} noValidate={true}>
<Form.Control
type="hidden"
key={fields.slug.key}
name={fields.slug.name}
defaultValue={fields.slug.initialValue}
/>
<div className="card-block">
<textarea
key={fields.body.key}
name={fields.body.name}
defaultValue={fields.body.initialValue}
placeholder="Write a comment..."
rows={3}
className="form-control"
></textarea>
<ErrorMessage messages={errors} />
</div>
<div className="card-footer">
{authorImage && <Image src={authorImage} alt="" className="comment-author-img" />}
<Button variant="primary" type="submit" disabled={isPending}>
Post Comment
</Button>
</div>
</Form>
</>
);
};
@@ -1,8 +0,0 @@
import { z } from "zod";
export const inputsSchema = z.object({
slug: z.string().max(200),
body: z.string().max(200),
});
export type Inputs = z.infer<typeof inputsSchema>;
@@ -1,21 +0,0 @@
import { fetchComments } from "@/modules/features/article/fetch/fetchComments";
import { fetchCurrentUser } from "@/modules/features/auth/fetch/fetchCurrentUser";
import { getSession } from "@/utils/auth/session";
import { CommentCard } from "../commentCard";
type Props = {
slug: string;
};
export const CommentList = async ({ slug }: Props) => {
const comments = await fetchComments(slug);
const currentUserPromise = (await getSession()) ? fetchCurrentUser() : undefined;
return (
<>
{comments.map((comment, index) => (
<CommentCard key={index} slug={slug} comment={comment} currentUserPromise={currentUserPromise} />
))}
</>
);
};
@@ -1 +0,0 @@
export * from "./commentList";
-31
View File
@@ -1,31 +0,0 @@
import { Col, Row } from "react-bootstrap";
import { Suspense } from "react";
import { fetchCurrentUser } from "@/modules/features/auth/fetch/fetchCurrentUser";
import { getSession } from "@/utils/auth/session";
import { ArticleArea } from "./_components/articleArea";
import { CommentForm } from "./_components/commentForm";
import { CommentList } from "./_components/commentList";
type Params = Promise<{
slug: string;
}>;
const Page = async (props: { params: Params }) => {
const currentUserPromise = (await getSession()) ? fetchCurrentUser() : undefined;
const params = await props.params;
return (
<ArticleArea slug={params.slug}>
<Row>
<Col xs={12} md={{ span: 8, offset: 2 }}>
<CommentForm slug={params.slug} currentUserPromise={currentUserPromise} />
<Suspense fallback={<p>Loading comments...</p>}>
<CommentList slug={params.slug} />
</Suspense>
</Col>
</Row>
</ArticleArea>
);
};
export default Page;
-78
View File
@@ -1,78 +0,0 @@
import { Container, Col, Row } from "react-bootstrap";
import { getSession } from "@/utils/auth/session";
import { ExhaustiveError } from "@/utils/errors";
import clsx from "clsx";
import Link from "next/link";
import { Suspense } from "react";
import { FeedArticleList } from "../_components/feedArticleList";
import { GlobalArticleList } from "../_components/globalArticleList";
import { TagArticleList } from "../_components/tagArticleList";
import { TagList } from "../_components/tagList";
import { SearchParams } from "../_types";
//import { ArticlesList } from "./_components/articlesList";
const ArticleList = (searchParams: SearchParams) => {
switch (searchParams.tab) {
case "yours":
return <FeedArticleList currentPage={searchParams.page} />;
case "global":
return <GlobalArticleList currentPage={searchParams.page} />;
case "tag":
return <TagArticleList currentPage={searchParams.page} tag={searchParams.tag ?? ""} />;
default:
throw new ExhaustiveError(searchParams.tab, "all tab cases are not covered");
}
};
const Page = async (props: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) => {
const session = await getSession();
const searchParams = SearchParams.parse(await props.searchParams);
return (
<section className="auth-page">
<Container className="page">
<hr className="col-1 my-5 mx-0" />
<Row>
<Col md={9}>
<div className="feed-toggle">
<ul className="nav nav-pills outline-active">
{session && (
<li className="nav-item">
<Link className={clsx("nav-link", searchParams.tab === "yours" && "active")} href="/?tab=yours">
Your Feed
</Link>
</li>
)}
<li className="nav-item">
<Link className={clsx("nav-link", searchParams.tab === "global" && "active")} href="/?tab=global">
Global Feed
</Link>
</li>
{searchParams.tag && (
<li className="nav-item">
<Link
className={clsx("nav-link", searchParams.tab === "tag" && "active")}
href={`/?tab=tag&tag=${searchParams.tag}`}
>
#{searchParams.tag}
</Link>
</li>
)}
</ul>
</div>
<Suspense key={JSON.stringify(searchParams)} fallback={<p>Chargement...</p>}>
<ArticleList {...searchParams} />
</Suspense>
</Col>
<Col md={3}>
<Suspense fallback={<p>Chargement...</p>}>
<TagList />
</Suspense>
</Col>
</Row>
</Container>
</section>
);
};
export default Page;
-30
View File
@@ -1,30 +0,0 @@
import { Container, Col, Row } from "react-bootstrap";
import { UpdateArticleEditor } from "@/modules/features/article/components/articleEditor";
import { fetchArticle } from "@/modules/features/article/fetch/fetchArticle";
import { Suspense } from "react";
type Props = {
params: Promise<{
slug: string;
}>;
};
const Page = async ({ params }: Props) => {
const article = fetchArticle((await params).slug);
return (
<div className="editor-page">
<Container className="page">
<Row>
<Col xs={12} md={{ span: 10, offset: 1 }}>
<Suspense fallback={<p> Chargement...</p>}>
<UpdateArticleEditor defaultValues={article} />
</Suspense>
</Col>
</Row>
</Container>
</div>
);
};
export default Page;
-16
View File
@@ -1,16 +0,0 @@
import { Container, Col, Row } from "react-bootstrap";
import { CreateArticleEditor } from "@/modules/features/article/components/articleEditor";
const Page = () => (
<section className="editor-page">
<Container className="page">
<Row>
<Col xs={12} md={{ span: 10, offset: 1 }}>
<CreateArticleEditor />
</Col>
</Row>
</Container>
</section>
);
export default Page;
-37
View File
@@ -1,37 +0,0 @@
"use client";
//import { Button } from "@/modules/common/components/button";
import { Container, Button, Col, Row } from "react-bootstrap";
import TimeClock from "./_components/timeClock/timeClock";
const Error = ({
reset,
}: {
error: Error & {
digest?: string;
};
reset: () => void;
}) => {
return (
<section className="error-page">
<Container className="banner">
<Row>
<Col sm={6} md={7} lg={8}>
<h1 className="mt-4"> Page introuvable!</h1>
<p className="fs-4">La page que vous vouliez consulter n&apos;est pas d&apos;isponible pour le moment.</p>
</Col>
<Col sm={6} md={5} lg={4}>
<TimeClock />
</Col>
</Row>
</Container>
<Container className="page">
<Button type="button" onClick={() => reset()}>
Try again
</Button>
</Container>
</section>
);
};
export default Error;
-72
View File
@@ -1,72 +0,0 @@
import { Footer } from "@/modules/common/components/footer";
import { Header } from "@/modules/features/auth/components/header";
//import "@/styles/font.css";
import "@/styles/styles.scss";
import "@/styles/icomoon.scss";
//import "ionicons/css/ionicons.min.css";
import type { Viewport } from "next";
import { ReactNode } from "react";
export const fetchCache = "default-no-store";
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
themeColor: "#00a3ff",
}
export async function generateMetadata() {
return {
title: "Ad Astra",
description: "Ad Astra, fleurs et résines de CBD et autre cannabinoïdes.",
manifest: "/favicon/manifest.json",
icons: {
icon: [
{ url: '/favicon/android-icon-192x192.png', sizes: '192x192', type: 'image/png' },
{ url: '/favicon/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/favicon/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
{ url: '/favicon/favicon-96x96.png', sizes: '96x96', type: 'image/png' },
],
shortcut: [
{ url: '/favicon/favicon.ico', type:'image/vnd.microsoft.icon'}
],
apple: [
{ url: '/favicon/apple-icon-57x57.png', sizes: '57x57', type: 'image/png' },
{ url: '/favicon/apple-icon-60x60.png', sizes: '60x60', type: 'image/png' },
{ url: '/favicon/apple-icon-72x72.png', sizes: '72x72', type: 'image/png' },
{ url: '/favicon/apple-icon-76x76.png', sizes: '76x76', type: 'image/png' },
{ url: '/favicon/apple-icon-114x114.png', sizes: '114x114', type: 'image/png' },
{ url: '/favicon/apple-icon-120x120.png', sizes: '120x120', type: 'image/png' },
{ url: '/favicon/apple-icon-144x144.png', sizes: '144x144', type: 'image/png' },
{ url: '/favicon/apple-icon-180x180.png', sizes: '180x180', type: 'image/png' },
],
},
other: {
'msapplication-TileColor': '#da532c',
'msapplication-TileImage': `${'images/tileimage.jpg'}`,
'msapplication-config': '/favicon/browserconfig.xml',
}
}
}
const RootLayout = ({
children,
}: Readonly<{
children: ReactNode;
}>) => {
return (
<html className="h-100" lang="en">
<body className="d-flex flex-column h-100" data-bs-theme="light">
<Header />
<main className="h-100">
{children}
</main>
<Footer />
</body>
</html>
);
};
export default RootLayout;
@@ -1,47 +0,0 @@
"use server";
import { createApiClient } from "@/utils/api/apiClient";
import { createSession } from "@/utils/auth/session";
import { parseWithZod } from "@conform-to/zod";
import { redirect } from "next/navigation";
import { inputsSchema } from "./types";
export const signInAction = async (_prevState: unknown, formData: FormData) => {
const submission = parseWithZod(formData, {
schema: inputsSchema,
});
if (submission.status !== "success") {
return submission.reply();
}
const client = createApiClient({
path: "/users/login",
method: "post",
params: {
body: {
user: submission.value,
},
},
});
const response = await client.sendRequest();
if (response.result === "success") {
await createSession(response.data.user.token);
redirect("/");
}
switch (response.statusCode) {
case 401:
return submission.reply({
formErrors: ["Login failed. The email or password is incorrect."],
});
case 422:
return submission.reply({
formErrors: Object.values(response.error.errors).flat(),
});
default:
throw new Error("api error");
}
};
@@ -1,11 +0,0 @@
"use client";
import { LoginForm as LoginFormPresentation } from "./presentation";
import { signInAction } from "./action";
import { useActionState } from "react";
export const LoginForm = () => {
const [state, action, isPending] = useActionState(signInAction, undefined);
return <LoginFormPresentation result={state} action={action} isPending={isPending} />;
};
@@ -1 +0,0 @@
export * from "./container";
@@ -1,67 +0,0 @@
import { Button, Form } from "react-bootstrap";
import { ErrorMessage } from "@/modules/common/components/errorMessage";
import { SubmissionResult, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { startTransition } from "react";
import { inputsSchema } from "./types";
type Props = {
action?: (formData: FormData) => void;
isPending?: boolean;
result?: SubmissionResult<string[]>;
};
export const LoginForm = ({ result, action, isPending }: Props) => {
const [form, fields] = useForm({
lastResult: result,
onValidate({ formData }) {
return parseWithZod(formData, { schema: inputsSchema });
},
// onSubmit is defined to avoid resetting form after successful submission
// see https://github.com/edmundhung/conform/discussions/606
onSubmit(event, { formData }) {
event.preventDefault();
startTransition(() => {
action?.(formData);
});
},
shouldValidate: "onBlur",
shouldRevalidate: "onBlur",
});
return (
<>
<ErrorMessage messages={form.errors} />
<Form id={form.id} action={action} onSubmit={form.onSubmit} noValidate={true}>
<Form.Group className="mb-3" controlId="formEmail">
<Form.Label>Adresse email</Form.Label>
<Form.Control
type="email"
key={fields.email.key}
name={fields.email.name}
defaultValue={fields.email.initialValue}
placeholder="Votre adresse email"
/>
<ErrorMessage messages={fields.email.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formPassword">
<Form.Label>Mot de passe</Form.Label>
<Form.Control
type="password"
key={fields.password.key}
name={fields.password.name}
defaultValue={fields.password.initialValue}
placeholder="Mot de passe"
autoComplete="new-password"
/>
<ErrorMessage messages={fields.password.errors} />
</Form.Group>
<div className="d-flex justify-content-end">
<Button variant="primary" type="submit" disabled={isPending}>
Se connecter
</Button>
</div>
</Form>
</>
);
};
@@ -1,8 +0,0 @@
import { z } from "zod";
export const inputsSchema = z.object({
email: z.string().email(),
password: z.string(),
});
export type Inputs = z.infer<typeof inputsSchema>;
-20
View File
@@ -1,20 +0,0 @@
import { Container, Col, Row } from "react-bootstrap";
import Link from "next/link";
import { LoginForm } from "./_components/loginForm";
const Page = () => (
<section className="auth-page">
<Container className="page">
<Row>
<Col xs={12} md={{ span: 6, offset: 3 }}>
<h1 className="text-xs-center">Se connecter</h1>
<p className="text-xs-center">
<Link href="/register">Pas encore de compte?</Link>
</p>
<LoginForm />
</Col>
</Row>
</Container>
</section>
);
export default Page;
-9
View File
@@ -1,9 +0,0 @@
"use server";
import { deleteSession } from "@/utils/auth/session";
import { redirect } from "next/navigation";
export async function GET() {
await deleteSession();
redirect("/login");
}
-13
View File
@@ -1,13 +0,0 @@
import { Container, Button } from "react-bootstrap";
const NotFound = () => {
return (
<section className="not-found-page">
<Container className="page">
<p className="error-message"> Cette page n&apos;existe pas.</p>
</Container>
</section>
);
};
export default NotFound;
-23
View File
@@ -1,23 +0,0 @@
import { Container, Col, Row } from "react-bootstrap";
import TimeClock from "./_components/timeClock/timeClock";
const Page = async (props: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) => {
return (
<div className="home-page">
<Container className="banner">
<Row>
<Col sm={6} md={7} lg={8}>
<h1 className="mt-4">Ad Astra, bientôt en ligne!</h1>
<p className="fs-4">Encore un peu de patience, notre site sera disponible d&apos;ici peu.</p>
</Col>
<Col sm={6} md={5} lg={4}>
<TimeClock />
</Col>
</Row>
</Container>
</div>
);
};
export default Page;
@@ -1 +0,0 @@
export * from "./myArticleList";
@@ -1,34 +0,0 @@
import { Pagination, PaginationItem } from "@/modules/common/components/pagination";
import { calcTotalPageNumber } from "@/modules/common/functions/pagination";
import { ArticleCard } from "@/modules/features/article/components/articleCard";
import { fetchArticlesByAuthor } from "@/modules/features/article/fetch/fetchArticleList";
const Pages = ({ totalPages, currentPage }: { totalPages: number; currentPage: number }) => (
<Pagination>
{[...Array(totalPages)].map((_, index) => {
const page = index + 1;
const href = `/?tab=global&page=${page}`;
return (
<PaginationItem href={href} active={page === currentPage} key={index}>
{page}
</PaginationItem>
);
})}
</Pagination>
);
export const MyArticleList = async ({ username, currentPage }: { currentPage: number; username: string }) => {
const { articles, articlesCount } = await fetchArticlesByAuthor(username, currentPage);
const totalPages = calcTotalPageNumber(articlesCount, 10);
return articles.length < 1 ? (
<p>No articles found.</p>
) : (
<>
{articles.map((article, index) => (
<ArticleCard key={index} article={article} />
))}
{1 < totalPages && <Pages currentPage={currentPage} totalPages={totalPages} />}
</>
);
};
@@ -1,13 +0,0 @@
import { User } from "@/utils/types/models";
export const showEditProfileSettingsButton = (profileUsername: string, currentUser: User | undefined) => {
if (currentUser == null) {
return false;
}
return profileUsername === currentUser.username;
};
export const showFollowButton = (profileUsername: string, currentUser: User | undefined) => {
return profileUsername !== currentUser?.username;
};
@@ -1,7 +0,0 @@
import { z } from "zod";
export const searchParamsSchema = z.object({
page: z.coerce.number().min(1).catch(1),
});
export type SearchParams = z.infer<typeof searchParamsSchema>;
@@ -1,34 +0,0 @@
import { Pagination, PaginationItem } from "@/modules/common/components/pagination";
import { calcTotalPageNumber } from "@/modules/common/functions/pagination";
import { ArticleCard } from "@/modules/features/article/components/articleCard";
import { fetchFavoriteArticles } from "@/modules/features/article/fetch/fetchArticleList";
const Pages = ({ totalPages, currentPage }: { totalPages: number; currentPage: number }) => (
<Pagination>
{[...Array(totalPages)].map((_, index) => {
const page = index + 1;
const href = `/?tab=global&page=${page}`;
return (
<PaginationItem href={href} active={page === currentPage} key={index}>
{page}
</PaginationItem>
);
})}
</Pagination>
);
export const FavoriteArticleList = async ({ username, currentPage }: { currentPage: number; username: string }) => {
const { articles, articlesCount } = await fetchFavoriteArticles(username, currentPage);
const totalPages = calcTotalPageNumber(articlesCount, 10);
return articles.length < 1 ? (
<p>No articles found.</p>
) : (
<>
{articles.map((article, index) => (
<ArticleCard key={index} article={article} />
))}
{1 < totalPages && <Pages currentPage={currentPage} totalPages={totalPages} />}
</>
);
};
@@ -1 +0,0 @@
export * from "./favoriteArticleList";
@@ -1,7 +0,0 @@
import { z } from "zod";
export const SearchParams = z.object({
page: z.coerce.number().min(1).catch(1),
});
export type SearchParams = z.infer<typeof SearchParams>;
@@ -1,39 +0,0 @@
import { decodePathParams } from "@/modules/common/functions/url";
import Link from "next/link";
import { Suspense } from "react";
import { FavoriteArticleList } from "./_components/favoriteArticleList";
import { SearchParams } from "./_types/types";
const FavoritesPage = async (props: {
params: Promise<{
username: string;
}>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) => {
const searchParams = SearchParams.parse(await props.searchParams);
const pathParams = decodePathParams(await props.params);
return (
<>
<div className="articles-toggle">
<ul className="nav nav-pills outline-active">
<li className="nav-item">
<Link className="nav-link" href={`/profile/${pathParams.username}`}>
My Articles
</Link>
</li>
<li className="nav-item">
<Link className="nav-link active" href={`/profile/${pathParams.username}/favorites`}>
Favorited Articles
</Link>
</li>
</ul>
</div>
<Suspense key={JSON.stringify(searchParams)} fallback={<p>Loading...</p>}>
<FavoriteArticleList currentPage={searchParams.page} username={pathParams.username} />
</Suspense>
</>
);
};
export default FavoritesPage;
-50
View File
@@ -1,50 +0,0 @@
import { ReactNode } from "react";
import Image from "next/image";
import { Button } from "@/modules/common/components/button";
import { fetchCurrentUser } from "@/modules/features/auth/fetch/fetchCurrentUser";
import { FollowButton } from "@/modules/features/profile/components/followButton";
import { fetchProfile } from "@/modules/features/profile/fetch/fetchProfile";
import { getSession } from "@/utils/auth/session";
import { showEditProfileSettingsButton, showFollowButton } from "./_functions";
type Props = {
children: ReactNode;
params: Promise<{
username: string;
}>;
};
const Layout = async ({ children, params }: Props) => {
const profile = await fetchProfile((await params).username);
const currentUser = (await getSession()) ? await fetchCurrentUser() : undefined;
return (
<div className="profile-page">
<div className="user-info">
<div className="container">
<div className="row">
<div className="col-xs-12 col-md-10 offset-md-1">
{profile.image && <Image src={profile.image} className="user-img" alt="" />}
<h4>{profile.username}</h4>
{profile.bio && <p>{profile.bio}</p>}
{showFollowButton(profile.username, currentUser) && <FollowButton {...profile} color="secondary" />}
{showEditProfileSettingsButton(profile.username, currentUser) && (
<Button component="a" href="/settings" className="action-btn" color="secondary">
<i className="ion-plus-round"></i> Edit Profile Settings
</Button>
)}
</div>
</div>
</div>
</div>
<div className="container">
<div className="row">
<div className="col-xs-12 col-md-10 offset-md-1">{children}</div>
</div>
</div>
</div>
);
};
export default Layout;
-39
View File
@@ -1,39 +0,0 @@
import { decodePathParams } from "@/modules/common/functions/url";
import Link from "next/link";
import { Suspense } from "react";
import { MyArticleList } from "./_components/myArticleList";
import { searchParamsSchema } from "./_types";
const ProfilePage = async (props: {
params: Promise<{
username: string;
}>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) => {
const pathParams = decodePathParams(await props.params);
const searchParams = searchParamsSchema.parse(await props.searchParams);
return (
<>
<div className="articles-toggle">
<ul className="nav nav-pills outline-active">
<li className="nav-item">
<Link className="nav-link active" href={`/profile/${pathParams.username}`}>
My Articles
</Link>
</li>
<li className="nav-item">
<Link className="nav-link" href={`/profile/${pathParams.username}/favorites`}>
Favorited Articles
</Link>
</li>
</ul>
</div>
<Suspense key={JSON.stringify(searchParams)} fallback={<p>Loading...</p>}>
<MyArticleList currentPage={searchParams.page} username={pathParams.username} />
</Suspense>
</>
);
};
export default ProfilePage;
@@ -1,43 +0,0 @@
"use server";
import { createApiClient } from "@/utils/api/apiClient";
import { createSession } from "@/utils/auth/session";
import { parseWithZod } from "@conform-to/zod";
import { redirect } from "next/navigation";
import { inputsSchema } from "./types";
export const signUpAction = async (_prevState: unknown, formData: FormData) => {
const submission = parseWithZod(formData, {
schema: inputsSchema,
});
if (submission.status !== "success") {
return submission.reply();
}
const client = createApiClient({
path: "/users",
method: "post",
params: {
body: {
user: submission.value,
},
},
});
const response = await client.sendRequest();
if (response.result === "success") {
createSession(response.data.user.token);
redirect("/");
}
switch (response.statusCode) {
case 422:
return submission.reply({
formErrors: Object.values(response.error.errors).flat(),
});
default:
throw new Error("api error");
}
};
@@ -1,11 +0,0 @@
"use client";
import { useActionState } from "react";
import { signUpAction } from "./action";
import { RegistrationForm as RegistrationFormPresentation } from "./presentation";
export const RegistrationForm = () => {
const [state, dispatch, isPending] = useActionState(signUpAction, undefined);
return <RegistrationFormPresentation result={state} action={dispatch} isPending={isPending} />;
};
@@ -1 +0,0 @@
export * from "./container";
@@ -1,101 +0,0 @@
import { Button, Form } from "react-bootstrap";
//import { Button } from "@/modules/common/components/button";
import { ErrorMessage } from "@/modules/common/components/errorMessage";
import { SubmissionResult, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { startTransition } from "react";
import { inputsSchema } from "./types";
type Props = {
action?: (formData: FormData) => void;
isPending?: boolean;
result?: SubmissionResult<string[]>;
};
export const RegistrationForm = ({ result, action, isPending }: Props) => {
const [form, fields] = useForm({
lastResult: result,
onValidate({ formData }) {
return parseWithZod(formData, { schema: inputsSchema });
},
// onSubmit is defined to avoid resetting form after successful submission
// see https://github.com/edmundhung/conform/discussions/606
onSubmit(event, { formData }) {
event.preventDefault();
startTransition(() => {
action?.(formData);
});
},
shouldValidate: "onBlur",
shouldRevalidate: "onBlur",
});
return (
<>
<ErrorMessage messages={form.errors} />
<Form id={form.id} action={action} onSubmit={form.onSubmit} noValidate={true}>
<Form.Group className="mb-3" controlId="formFirstname">
<Form.Label>Prénom</Form.Label>
<Form.Control
type="text"
key={fields.lastname.key}
name={fields.lastname.name}
defaultValue={fields.lastname.initialValue}
placeholder="Votre prénom"
/>
<ErrorMessage messages={fields.firstname.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formLastname">
<Form.Label>Nom</Form.Label>
<Form.Control
type="text"
key={fields.firstname.key}
name={fields.firstname.name}
defaultValue={fields.firstname.initialValue}
placeholder="Votre nom"
/>
<ErrorMessage messages={fields.lastname.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formUsername">
<Form.Label>Pseudo</Form.Label>
<Form.Control
type="text"
key={fields.username.key}
name={fields.username.name}
defaultValue={fields.username.initialValue}
placeholder="Votre pseudo"
/>
<ErrorMessage messages={fields.username.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formEmail">
<Form.Label>Adresse email</Form.Label>
<Form.Control
type="email"
key={fields.email.key}
name={fields.email.name}
defaultValue={fields.email.initialValue}
placeholder="Votre adresse email"
/>
<ErrorMessage messages={fields.email.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formPassword">
<Form.Label>Mot de passe</Form.Label>
<Form.Control
type="password"
key={fields.password.key}
name={fields.password.name}
defaultValue={fields.password.initialValue}
placeholder="Mot de passe"
autoComplete="new-password"
/>
<ErrorMessage messages={fields.password.errors} />
</Form.Group>
<div className="d-flex justify-content-end">
<Button variant="primary" type="submit" disabled={isPending}>
Créer mon compte
</Button>
</div>
</Form>
</>
);
};
@@ -1,11 +0,0 @@
import { z } from "zod";
export const inputsSchema = z.object({
username: z.string().max(200),
firstname: z.string().max(200),
lastname: z.string().max(200),
email: z.string().email(),
password: z.string().min(4).max(200),
});
export type Inputs = z.infer<typeof inputsSchema>;
-21
View File
@@ -1,21 +0,0 @@
import { Container, Col, Row } from "react-bootstrap";
import Link from "next/link";
import { RegistrationForm } from "./_components/registrationForm";
const Page = () => (
<section className="auth-page">
<Container className="page">
<Row>
<Col xs={12} md={{ span: 6, offset: 3 }}>
<h1 className="text-xs-center">Créer un compte</h1>
<p className="text-xs-center">
<Link href="/login">Déjà un compte?</Link>
</p>
<RegistrationForm />
</Col>
</Row>
</Container>
</section>
);
export default Page;
-45
View File
@@ -1,45 +0,0 @@
"use server";
import { createApiClient } from "@/utils/api/apiClient";
import { redirect } from "next/navigation";
import { inputsSchema } from "./types";
import { createSession } from "@/utils/auth/session";
import { parseWithZod } from "@conform-to/zod";
import { SubmissionResult } from "@conform-to/react";
export const updateSettingsAction = async (_prevState: SubmissionResult<string[]> | undefined, formData: FormData) => {
const submission = parseWithZod(formData, {
schema: inputsSchema,
});
if (submission.status !== "success") {
return submission.reply();
}
const client = createApiClient({
path: "/user",
method: "put",
params: {
body: {
user: submission.value,
},
},
});
const response = await client.sendRequest();
if (response.result === "success") {
const user = response.data.user;
await createSession(user.token);
redirect(`/profile/${user.username}`);
}
switch (response.statusCode) {
case 422:
return submission.reply({
formErrors: Object.values(response.error.errors).flat(),
});
default:
throw new Error("api error");
}
};
@@ -1,12 +0,0 @@
"use client";
import { use, useActionState } from "react";
import { SettingsForm as SettingsFormPresentation } from "./presentation";
import { updateSettingsAction } from "./action";
import { User } from "@/utils/types/models";
export const SettingsForm = ({ user }: { user: Promise<User> }) => {
const [state, action, isPending] = useActionState(updateSettingsAction, undefined);
return <SettingsFormPresentation user={use(user)} result={state} action={action} isPending={isPending} />;
};
-1
View File
@@ -1 +0,0 @@
export * from "./container";
@@ -1,123 +0,0 @@
import { Button, Form } from "react-bootstrap";
import { ErrorMessage } from "@/modules/common/components/errorMessage";
import { User } from "@/utils/types/models";
import { SubmissionResult, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { startTransition } from "react";
import { inputsSchema } from "./types";
type Props = {
user?: User;
action?: (formData: FormData) => void;
isPending?: boolean;
result?: SubmissionResult<string[]>;
};
export const SettingsForm = ({ user, action, isPending, result }: Props) => {
const [form, fields] = useForm({
lastResult: result,
onValidate({ formData }) {
return parseWithZod(formData, { schema: inputsSchema });
},
// onSubmit is defined to avoid resetting form after successful submission
// see https://github.com/edmundhung/conform/discussions/606
onSubmit(event, { formData }) {
event.preventDefault();
startTransition(() => {
action?.(formData);
});
},
defaultValue: { ...user },
shouldValidate: "onBlur",
shouldRevalidate: "onBlur",
});
return (
<>
<ErrorMessage messages={form.errors} />
<Form id={form.id} action={action} onSubmit={form.onSubmit} noValidate={true}>
<Form.Group className="mb-3" controlId="formUsername">
<Form.Label>Pseudo</Form.Label>
<Form.Control
type="text"
key={fields.username.key}
name={fields.username.name}
defaultValue={fields.username.initialValue}
placeholder="Votre pseudo"
/>
<ErrorMessage messages={fields.username.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formEmail">
<Form.Label>Adresse email</Form.Label>
<Form.Control
type="email"
key={fields.email.key}
name={fields.email.name}
defaultValue={fields.email.initialValue}
placeholder="Votre adresse email"
/>
<ErrorMessage messages={fields.email.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formFirstname">
<Form.Label>Prénom</Form.Label>
<Form.Control
type="text"
key={fields.firstname.key}
name={fields.firstname.name}
defaultValue={fields.firstname.initialValue}
placeholder="Votre prénom"
/>
<ErrorMessage messages={fields.firstname.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formLastname">
<Form.Label>Nom</Form.Label>
<Form.Control
type="text"
key={fields.lastname.key}
name={fields.lastname.name}
defaultValue={fields.lastname.initialValue}
placeholder="Votre nom"
/>
<ErrorMessage messages={fields.lastname.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formPassword">
<Form.Label>Mot de passe</Form.Label>
<Form.Control
type="password"
key={fields.password.key}
name={fields.password.name}
defaultValue={fields.password.initialValue}
placeholder="Nouveau mot de passe"
autoComplete="new-password"
/>
<ErrorMessage messages={fields.password.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formBio">
<Form.Label>Bio</Form.Label>
<Form.Control as="textarea" rows={8}
key={fields.bio.key}
name={fields.bio.name}
defaultValue={fields.bio.initialValue}
placeholder="Courte description"
/>
<ErrorMessage messages={fields.bio.errors} />
</Form.Group>
<Form.Group className="mb-3" controlId="formImage">
<Form.Label>Image</Form.Label>
<Form.Control
type="text"
key={fields.image.key}
name={fields.image.name}
defaultValue={fields.image.initialValue}
/>
<ErrorMessage messages={fields.image.errors} />
</Form.Group>
<div className="d-flex justify-content-end">
<Button variant="primary" type="submit" disabled={isPending}>
Enregistrer
</Button>
</div>
</Form>
</>
);
};
-13
View File
@@ -1,13 +0,0 @@
import { z } from "zod";
export const inputsSchema = z.object({
username: z.string().max(200),
email: z.string().max(200).email(),
firstname: z.string().max(200),
lastname: z.string().max(200),
bio: z.string().max(200).optional(),
image: z.string().max(200).optional(),
password: z.string().min(4).max(200).optional(),
});
export type Inputs = z.infer<typeof inputsSchema>;
-28
View File
@@ -1,28 +0,0 @@
import { Container, Col, Row } from "react-bootstrap";
import { LogoutButton } from "@/modules/features/auth/components/logoutButton";
import { fetchCurrentUser } from "@/modules/features/auth/fetch/fetchCurrentUser";
import { Suspense } from "react";
import { SettingsForm } from "./_components";
const Page = async () => {
const user = fetchCurrentUser();
return (
<section className="settings-page">
<Container className="page">
<Row>
<Col xs={12} md={{ span: 6, offset: 3 }}>
<h1 className="text-xs-center">Vos paramètres</h1>
<Suspense fallback={<p> Chargement...</p>}>
<SettingsForm user={user} />
</Suspense>
<hr />
<LogoutButton />
</Col>
</Row>
</Container>
</section>
);
};
export default Page;