feat: account page content

This commit is contained in:
Takeshi Kimata
2024-06-03 21:53:52 +09:00
parent 24fe7bebb3
commit e4db79047a
9 changed files with 151 additions and 40 deletions

View File

@@ -9,11 +9,20 @@ module.exports = function (sequelize) {
router.get('/', verifySignedIn, async (req, res) => { router.get('/', verifySignedIn, async (req, res) => {
try { try {
const projects = await Project.findAll({ let projects;
if (req.query.onlyUserProjects === 'true') {
projects = await Project.findAll({
where: {
userId: req.userId,
},
});
} else {
projects = await Project.findAll({
where: { where: {
[Op.or]: [{ isPublic: true }, { userId: req.userId }], [Op.or]: [{ isPublic: true }, { userId: req.userId }],
}, },
}); });
}
res.json(projects); res.json(projects);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@@ -0,0 +1,19 @@
import { Chip } from '@nextui-org/react';
type Props = {
isPublic: boolean;
publicText: string;
privateText: string;
};
export default function PublicityChip({ isPublic, publicText, privateText }: Props) {
return isPublic ? (
<Chip size="sm" variant="bordered">
{publicText}
</Chip>
) : (
<Chip size="sm" variant="bordered">
{privateText}
</Chip>
);
}

View File

@@ -53,7 +53,10 @@
"email_not_exist": "Email address not found", "email_not_exist": "Email address not found",
"signup_error": "Sign up failed", "signup_error": "Sign up failed",
"signin_error": "Sign in failed", "signin_error": "Sign in failed",
"your_projects": "Your Projects" "your_projects": "Your Projects",
"public": "Public",
"private": "Private",
"no_projects_found": "No projects found"
}, },
"Admin": { "Admin": {
"user_management": "User Management", "user_management": "User Management",

View File

@@ -52,7 +52,10 @@
"email_not_exist": "メールアドレスが見つかりません", "email_not_exist": "メールアドレスが見つかりません",
"signup_error": "新規登録に失敗しました", "signup_error": "新規登録に失敗しました",
"signin_error": "サインインに失敗しました", "signin_error": "サインインに失敗しました",
"your_projects": "保有プロジェクト" "your_projects": "保有プロジェクト",
"public": "パブリック",
"private": "プライベート",
"no_projects_found": "プロジェクトがありません"
}, },
"Admin": { "Admin": {
"user_management": "ユーザー管理", "user_management": "ユーザー管理",

View File

@@ -1,11 +1,18 @@
'use client'; 'use client';
import { useContext } from 'react'; import { useState, useEffect, useContext } from 'react';
import { Card, CardHeader, CardBody, Divider } from '@nextui-org/react'; import { Link, NextUiLinkClasses } from '@/src/navigation';
import { Card, CardHeader, CardFooter } from '@nextui-org/react';
import { TokenContext } from '@/utils/TokenProvider'; import { TokenContext } from '@/utils/TokenProvider';
import Avatar from 'boring-avatars'; import Avatar from 'boring-avatars';
import { fetchMyProjects } from '@/utils/projectsControl';
import { ProjectType } from '@/types/project';
import PublicityChip from '@/components/PublicityChip';
type AccountPageMessages = { type AccountPageMessages = {
yourProjects: string; yourProjects: string;
public: string;
private: string;
noProjectsFound: string;
}; };
type Props = { type Props = {
@@ -15,11 +22,31 @@ type Props = {
export default function AccountPage({ messages, locale }: Props) { export default function AccountPage({ messages, locale }: Props) {
const context = useContext(TokenContext); const context = useContext(TokenContext);
const [myProjects, setMyProjects] = useState<ProjectType[]>([]);
useEffect(() => {
async function fetchDataEffect() {
if (!context.isSignedIn()) {
return;
}
try {
const data = await fetchMyProjects(context.token.access_token);
setMyProjects(data);
} catch (error: any) {
console.error('Error in effect:', error.message);
}
}
fetchDataEffect();
}, [context]);
return ( return (
<> <>
{context.isSignedIn() && ( {context.isSignedIn() && (
<Card className="w-[600px] mt-16 mx-3"> <div className="container mx-auto max-w-3xl pt-6 px-6 flex-grow">
<div className="w-full p-3 flex items-center justify-between">
<Card className="w-[600px]">
<CardHeader className="flex gap-6"> <CardHeader className="flex gap-6">
<Avatar <Avatar
size={48} size={48}
@@ -28,15 +55,36 @@ export default function AccountPage({ messages, locale }: Props) {
colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']} colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']}
/> />
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-2xl font-bold">{context.token.user.username}</p> <p className="text-xl font-bold">{context.token.user.username}</p>
<p className="text-lg text-default-500">{context.token.user.email}</p> <p className="text-lg text-default-500">{context.token.user.email}</p>
</div> </div>
</CardHeader> </CardHeader>
<Divider />
<CardBody className="overflow-visible px-4 pb-4">
<p className="text-default-500">{messages.yourProjects}</p>
</CardBody>
</Card> </Card>
</div>
<div className="w-full p-3">
<h3 className="font-bold mb-2">{messages.yourProjects}</h3>
{myProjects.length > 0 ? (
myProjects.map((myProject, myProjectsIndex) => {
return (
<Card key={myProject.id} className={`w-[600px] ${myProjectsIndex !== 0 ? 'mt-2' : ''}`}>
<CardHeader className="flex gap-6 pb-0">
<Link href={`/projects/${myProject.id}/home`} locale={locale} className={NextUiLinkClasses}>
{myProject.name}
</Link>
</CardHeader>
<CardFooter className="justify-between pt-0">
<p className="text-small text-default-500">{myProject.detail}</p>
<PublicityChip isPublic={true} publicText={messages.public} privateText={messages.private} />
</CardFooter>
</Card>
);
})
) : (
<div className="text-default-500">{messages.noProjectsFound}</div>
)}
</div>
</div>
)} )}
</> </>
); );

View File

@@ -4,6 +4,9 @@ export default function Page(params: { locale: string }) {
const t = useTranslations('Auth'); const t = useTranslations('Auth');
const messages = { const messages = {
yourProjects: t('your_projects'), yourProjects: t('your_projects'),
public: t('public'),
private: t('private'),
noProjectsFound: t('no_projects_found'),
}; };
return <AccountPage messages={messages} locale={params.locale} />; return <AccountPage messages={messages} locale={params.locale} />;

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, useCallback } from 'react'; import { useState, useMemo, useCallback } from 'react';
import { import {
Table, Table,
TableHeader, TableHeader,
@@ -7,7 +7,6 @@ import {
TableRow, TableRow,
TableCell, TableCell,
Button, Button,
Chip,
DropdownTrigger, DropdownTrigger,
Dropdown, Dropdown,
DropdownMenu, DropdownMenu,
@@ -18,6 +17,7 @@ import { Link, NextUiLinkClasses } from '@/src/navigation';
import { MoreVertical } from 'lucide-react'; import { MoreVertical } from 'lucide-react';
import { ProjectType, ProjectsMessages } from '@/types/project'; import { ProjectType, ProjectsMessages } from '@/types/project';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import PublicityChip from '@/components/PublicityChip';
type Props = { type Props = {
projects: ProjectType[]; projects: ProjectType[];
@@ -62,15 +62,7 @@ export default function ProjectsTable({ projects, onEditProject, onDeleteProject
case 'id': case 'id':
return <span>{cellValue}</span>; return <span>{cellValue}</span>;
case 'isPublic': case 'isPublic':
return cellValue ? ( return <PublicityChip isPublic={cellValue} publicText={messages.public} privateText={messages.private} />;
<Chip size="sm" variant="bordered">
{messages.public}
</Chip>
) : (
<Chip size="sm" variant="bordered">
{messages.private}
</Chip>
);
case 'name': case 'name':
const maxLength = 30; const maxLength = 30;
const truncatedDetail = truncateText(project.detail, maxLength); const truncatedDetail = truncateText(project.detail, maxLength);

View File

@@ -28,7 +28,7 @@ async function fetchProject(jwt: string, projectId: number) {
} }
/** /**
* fetch project records * fetch projects (public and user own projects)
*/ */
async function fetchProjects(jwt: string) { async function fetchProjects(jwt: string) {
const url = `${apiServer}/projects`; const url = `${apiServer}/projects`;
@@ -53,6 +53,32 @@ async function fetchProjects(jwt: string) {
} }
} }
/**
* fetch projects (user own projects)
*/
async function fetchMyProjects(jwt: string) {
const url = `${apiServer}/projects?onlyUserProjects=true`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
},
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error: any) {
console.error('Error fetching data:', error.message);
}
}
/** /**
* Create project * Create project
*/ */
@@ -146,4 +172,4 @@ async function deleteProject(jwt: string, projectId: number) {
} }
} }
export { fetchProject, fetchProjects, createProject, updateProject, deleteProject }; export { fetchProject, fetchProjects, fetchMyProjects, createProject, updateProject, deleteProject };

View File

@@ -62,6 +62,10 @@ async function fetchMyRoles(jwt: string) {
} }
function isProjectManager(projectRoles: ProjectRoleType[], projectId: number) { function isProjectManager(projectRoles: ProjectRoleType[], projectId: number) {
if (!projectRoles) {
return false;
}
const found = projectRoles.find((role) => { const found = projectRoles.find((role) => {
return role.projectId === projectId; return role.projectId === projectId;
}); });
@@ -83,6 +87,10 @@ function isProjectManager(projectRoles: ProjectRoleType[], projectId: number) {
} }
function isProjectDeveloper(projectRoles: ProjectRoleType[], projectId: number) { function isProjectDeveloper(projectRoles: ProjectRoleType[], projectId: number) {
if (!projectRoles) {
return false;
}
const found = projectRoles.find((role) => { const found = projectRoles.find((role) => {
return role.projectId === projectId; return role.projectId === projectId;
}); });