Skip to content
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
13 changes: 8 additions & 5 deletions web/app/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { motion, useAnimation } from "framer-motion";
import { useCallback, useEffect, useState } from "react";
import { MdThumbUp } from "react-icons/md";
import request from "~/api/request";
import { useMyID, useRecommended } from "~/api/user";
import { useAboutMe, useRecommended } from "~/api/user";
import { Card } from "~/components/Card";
import { DraggableCard } from "~/components/DraggableCard";
import FullScreenCircularProgress from "~/components/common/FullScreenCircularProgress";
Expand All @@ -17,8 +17,8 @@ export default function Home() {
const controls = useAnimation();
const [clickedButton, setClickedButton] = useState<string>("");
const {
state: { data: myId },
} = useMyID();
state: { data: currentUser },
} = useAboutMe();

const [_, rerender] = useState({});
const [recommended, setRecommended] = useState<
Expand Down Expand Up @@ -71,6 +71,9 @@ export default function Home() {
});
}, [controls, accept]);

if (currentUser == null) {
return <FullScreenCircularProgress />;
}
if (recommended == null) {
return <FullScreenCircularProgress />;
}
Expand All @@ -89,15 +92,15 @@ export default function Home() {
{nextUser && (
<div className="relative h-full w-full">
<div className="-translate-x-4 -translate-y-4 inset-0 z-0 mt-4 transform">
<Card displayedUser={nextUser} />
<Card displayedUser={nextUser} currentUser={currentUser} />
</div>
<motion.div
animate={controls}
className="absolute inset-0 z-10 mt-4 flex items-center justify-center"
>
<DraggableCard
displayedUser={displayedUser}
comparisonUserId={myId || undefined}
currentUser={currentUser}
onSwipeLeft={reject}
onSwipeRight={accept}
clickedButton={clickedButton}
Expand Down
6 changes: 5 additions & 1 deletion web/app/settings/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ export default function SettingsProfile() {
編集する
</Link>
</div>
<Card displayedUser={data} onFlip={(back) => setBack(back)} />
<Card
displayedUser={data}
currentUser={data}
onFlip={(back) => setBack(back)}
/>
</div>
</div>
)}
Expand Down
269 changes: 190 additions & 79 deletions web/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,200 @@
import ThreeSixtyIcon from "@mui/icons-material/ThreeSixty";
import { Chip } from "@mui/material";
import type { UserID, UserWithCoursesAndSubjects } from "common/types";
import { useState } from "react";
import type { UserWithCoursesAndSubjects } from "common/types";
import React, { useState, useRef, useEffect, useCallback } from "react";
import NonEditableCoursesTable from "./course/NonEditableCoursesTable";
import UserAvatar from "./human/avatar";

interface CardProps {
displayedUser: UserWithCoursesAndSubjects;
comparisonUserId?: UserID;
currentUser: UserWithCoursesAndSubjects;
onFlip?: (isBack: boolean) => void;
}

export function Card({ displayedUser, comparisonUserId, onFlip }: CardProps) {
const CardFront = ({ displayedUser, currentUser }: CardProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const interestsContainerRef = useRef<HTMLDivElement>(null);
const coursesContainerRef = useRef<HTMLDivElement>(null);
const [isHiddenInterestExist, setHiddenInterestExist] = useState(false);
const [isHiddenCourseExist, setHiddenCourseExist] = useState(false);

useEffect(() => {
const container = containerRef.current;
if (!container) return;

const resizeObserver = new ResizeObserver(() => {
calculateVisibleInterests();
calculateVisibleCourses();
});

resizeObserver.observe(container);

calculateVisibleInterests(); // 初期計算
calculateVisibleCourses(); // 初期計算

return () => resizeObserver.disconnect();
}, []);

const calculateVisibleCourses = useCallback(() => {
const courses = displayedUser.courses;
const container = coursesContainerRef.current;
if (!container) return;

const containerHeight = container.offsetHeight; // コンテナの高さを取得

// 一旦コンテナを初期化
container.innerHTML = "";
setHiddenCourseExist(false);

// courses を一致・非一致で分類
const matchingCourses = courses.filter((course) =>
currentUser.courses.some((c) => c.id === course.id),
);
const nonMatchingCourses = courses.filter(
(course) => !currentUser.courses.some((c) => c.id === course.id),
);

// courses を表示する flex コンテナ
const coursesContainer = document.createElement("div");
coursesContainer.classList.add("flex", "flex-wrap", "gap-2");
container.appendChild(coursesContainer);

// 一致しているコースを先に表示
for (const course of [...matchingCourses, ...nonMatchingCourses]) {
const isMatching = currentUser.courses.some((c) => c.id === course.id);

// 新しい div 要素を作成
const element = document.createElement("div");
element.textContent = course.name;

// スタイル適用(赤 or 灰色)
element.classList.add("badge", "badge-outline");
element.style.backgroundColor = isMatching ? "red" : "gray";
element.style.color = "white";

// 表示判定
if (coursesContainer.offsetHeight + 30 <= containerHeight) {
coursesContainer.appendChild(element);
} else {
setHiddenCourseExist;
}
}
}, [displayedUser, currentUser]);

const calculateVisibleInterests = useCallback(() => {
const interests = displayedUser.interestSubjects;
const container = interestsContainerRef.current;
if (!container) return;

const containerHeight = container.offsetHeight; // コンテナの高さを取得

// 一旦コンテナを初期化
container.innerHTML = "";
setHiddenInterestExist(false);

// interests を一致・非一致で分類
const matchingInterests = interests.filter((interest) =>
currentUser.interestSubjects.some((i) => i.name === interest.name),
);
const nonMatchingInterests = interests.filter(
(interest) =>
!currentUser.interestSubjects.some((i) => i.name === interest.name),
);

// interests を表示する flex コンテナ
const flexContainer = document.createElement("div");
flexContainer.classList.add("flex", "flex-wrap", "gap-2");
container.appendChild(flexContainer);

// 一致している興味分野を先に表示
for (const interest of [...matchingInterests, ...nonMatchingInterests]) {
const isMatching = currentUser.interestSubjects.some(
(i) => i.name === interest.name,
);

// 新しい div 要素を作成
const element = document.createElement("div");
element.textContent = interest.name;

// スタイル適用(赤 or 灰色)
element.classList.add("badge", "badge-outline");
element.style.backgroundColor = isMatching ? "red" : "gray";
element.style.color = "white";
element.style.overflow = "hidden";
element.style.whiteSpace = "nowrap";
element.style.textOverflow = "ellipsis";

// 表示判定
if (flexContainer.offsetHeight + 30 <= containerHeight) {
flexContainer.appendChild(element);
} else {
setHiddenInterestExist(true);
}
}
}, [displayedUser, currentUser]);

return (
<div className="flex h-full flex-col gap-5 overflow-clip border-2 border-primary bg-secondary p-5">
<div className="grid h-[20%] grid-cols-3 items-center">
<UserAvatar
pictureUrl={displayedUser.pictureUrl}
width="9dvh"
height="9dvh"
/>
<div className="col-span-2 grid grid-rows-3 items-center">
<p className="col-span-3 font-bold text-1xl">{displayedUser.name}</p>
<p className="col-span-1 text-1xl">{displayedUser.grade}</p>
<p className="col-span-2 text-1xl">{displayedUser.faculty}</p>
<p className="col-span-2 text-1xl">{displayedUser.department}</p>
</div>
</div>

<div className="flex h-[70%] w-full flex-col gap-2" ref={containerRef}>
<div
ref={interestsContainerRef}
className="width-full h-[50%] overflow-hidden"
>
<div />
{isHiddenInterestExist && (
<div className="badge badge-outline bg-gray-200 text-gray-700">
And More
</div>
)}
</div>

<div
ref={coursesContainerRef}
className="width-full h-[50%] overflow-hidden"
>
<div />
{isHiddenCourseExist && (
<div className="badge badge-outline bg-gray-200 text-gray-700">
And More
</div>
)}
</div>
</div>
</div>
);
};

const CardBack = ({ displayedUser, currentUser }: CardProps) => {
return (
<div className="flex h-full flex-col overflow-hidden border-2 border-primary bg-secondary p-4">
<div className="flex justify-center">
<p className="font-bold text-lg">{displayedUser?.name}</p>
</div>
<NonEditableCoursesTable
userId={displayedUser.id}
comparisonUserId={currentUser.id}
/>
<div className="mt-4 flex justify-center">
<ThreeSixtyIcon className="text-3xl" />
</div>
</div>
);
};

export function Card({ displayedUser, currentUser, onFlip }: CardProps) {
const [isDisplayingBack, setIsDisplayingBack] = useState(false);

const handleRotate = () => {
Expand Down Expand Up @@ -39,7 +222,7 @@ export function Card({ displayedUser, comparisonUserId, onFlip }: CardProps) {
transform: isDisplayingBack ? "rotateY(180deg)" : "rotateY(0deg)",
}}
>
<CardFront displayedUser={displayedUser} />
<CardFront displayedUser={displayedUser} currentUser={currentUser} />
</div>
<div
className="absolute h-full w-full"
Expand All @@ -48,81 +231,9 @@ export function Card({ displayedUser, comparisonUserId, onFlip }: CardProps) {
transform: isDisplayingBack ? "rotateY(0deg)" : "rotateY(-180deg)",
}}
>
<CardBack
displayedUser={displayedUser}
comparisonUserId={comparisonUserId}
/>
<CardBack displayedUser={displayedUser} currentUser={currentUser} />
</div>
</div>
</div>
);
}

const CardFront = ({ displayedUser }: CardProps) => {
return (
<div className="flex h-full flex-col justify-between gap-5 overflow-hidden border-2 border-primary bg-secondary p-5">
<div className="grid h-[30%] grid-cols-3 items-center">
<UserAvatar
pictureUrl={displayedUser.pictureUrl}
width="10dvh"
height="10dvh"
/>
<div className="col-span-2 ml-2 flex justify-center">
<span className="font-bold text-4xl">{displayedUser.name}</span>
</div>
</div>
<div className="grid grid-cols-6 items-center gap-4">
<Chip label="学部" size="small" className="col-span-1" />
<p className="col-span-5 text-xl">{displayedUser.faculty}</p>
</div>
<div className="grid grid-cols-6 items-center gap-4">
<Chip label="学科" size="small" className="col-span-1" />
<p
className={`col-span-5 text-xl ${displayedUser.department.length > 7 ? "text-xs" : "text-2xl"}`}
>
{displayedUser.department}
</p>
</div>
<div className="grid grid-cols-6 items-center gap-4">
<Chip label="性別" size="small" className="col-span-1" />
<p className="col-span-5 text-xl">{displayedUser.gender}</p>
</div>
<div className="grid grid-cols-6 items-center gap-4">
<Chip label="学年" size="small" className="col-span-1" />
<p className="col-span-5 text-xl">{displayedUser.grade}</p>
</div>
<div className="grid max-h-[32%] flex-1 grid-cols-6 gap-4">
<Chip label="自己紹介" size="small" className="col-span-1 text-sm" />
<p className="col-span-5 line-clamp-8 overflow-hidden text-sm">
{displayedUser.intro}
</p>
</div>
<p>TODO: これはサンプルです</p>
<ul>
{displayedUser.interestSubjects.map((subject) => (
<li key={subject.id}>{subject.name}</li>
))}
</ul>
<div className="flex justify-center">
<ThreeSixtyIcon className="text-3xl" />
</div>
</div>
);
};

const CardBack = ({ displayedUser, comparisonUserId }: CardProps) => {
return (
<div className="flex h-full flex-col overflow-hidden border-2 border-primary bg-secondary p-4">
<div className="flex justify-center">
<p className="font-bold text-lg">{displayedUser?.name}</p>
</div>
<NonEditableCoursesTable
userId={displayedUser.id}
comparisonUserId={comparisonUserId}
/>
<div className="mt-4 flex justify-center">
<ThreeSixtyIcon className="text-3xl" />
</div>
</div>
);
};
11 changes: 4 additions & 7 deletions web/components/DraggableCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import CloseIcon from "@mui/icons-material/Close";
import { Box, Typography } from "@mui/material";
import type { UserID, UserWithCoursesAndSubjects } from "common/types";
import type { UserWithCoursesAndSubjects } from "common/types";
import { motion, useMotionValue, useMotionValueEvent } from "framer-motion";
import { useCallback, useState } from "react";
import { MdThumbUp } from "react-icons/md";
Expand All @@ -10,15 +10,15 @@ const SWIPE_THRESHOLD = 30;

interface DraggableCardProps {
displayedUser: UserWithCoursesAndSubjects;
comparisonUserId?: UserID;
currentUser: UserWithCoursesAndSubjects;
onSwipeRight: () => void;
onSwipeLeft: () => void;
clickedButton: string;
}

export const DraggableCard = ({
displayedUser,
comparisonUserId,
currentUser,
onSwipeRight,
onSwipeLeft,
clickedButton,
Expand Down Expand Up @@ -136,10 +136,7 @@ export const DraggableCard = ({
whileTap={{ scale: 0.95 }}
>
<CardOverlay />
<Card
displayedUser={displayedUser}
comparisonUserId={comparisonUserId}
/>
<Card displayedUser={displayedUser} currentUser={currentUser} />
</motion.div>
</section>
</div>
Expand Down
Loading
Loading