diff --git a/apps/web/components/icons/gray-star.svg b/apps/web/components/icons/gray-star.svg new file mode 100644 index 0000000..d57700f --- /dev/null +++ b/apps/web/components/icons/gray-star.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/apps/web/components/icons/like.svg b/apps/web/components/icons/like.svg new file mode 100644 index 0000000..a517f3e --- /dev/null +++ b/apps/web/components/icons/like.svg @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/apps/web/components/icons/liked.svg b/apps/web/components/icons/liked.svg new file mode 100644 index 0000000..2b664e3 --- /dev/null +++ b/apps/web/components/icons/liked.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/apps/web/components/icons/reply.svg b/apps/web/components/icons/reply.svg new file mode 100644 index 0000000..7f08278 --- /dev/null +++ b/apps/web/components/icons/reply.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/apps/web/components/layouts/mainLayout.tsx b/apps/web/components/layouts/mainLayout.tsx index 8299e70..7d516c7 100644 --- a/apps/web/components/layouts/mainLayout.tsx +++ b/apps/web/components/layouts/mainLayout.tsx @@ -3,6 +3,7 @@ import { Header } from 'organisms/header/header'; import { ApolloProvider } from '@apollo/client'; import { graphClient } from '../../graphql/apolloClient'; import { UserProvider } from '@auth0/nextjs-auth0'; +import { Toaster } from 'sonner'; type MainLayoutProps = { children: ReactNode; @@ -10,11 +11,14 @@ type MainLayoutProps = { export const MainLayout = ({ children }: MainLayoutProps) => { return ( - - -
- {children} - - + <> + + + +
+ {children} + + + ); }; diff --git a/apps/web/components/molecules/commentItem/commentItem.tsx b/apps/web/components/molecules/commentItem/commentItem.tsx new file mode 100644 index 0000000..07e7b71 --- /dev/null +++ b/apps/web/components/molecules/commentItem/commentItem.tsx @@ -0,0 +1,47 @@ +import { Card } from 'ui'; +import { getTimeDiffFromTimestamp } from 'utils/timeDifference/getTimeDiffFromTimestamp'; +import type { Comment } from '../../../types/types'; +import { ReactComponent as Reply } from '/components/icons/reply.svg'; +import { ReactComponent as Like } from '/components/icons/like.svg'; +import { UserAvatar } from 'molecules/userAvatar/userAvatar'; + +type CommentItemProps = Comment; + +export const CommentItem = ({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + id, + author, + comment, + timestamp, +}: CommentItemProps) => { + return ( +
+ +
+ +
+
+ {author.name} + + {getTimeDiffFromTimestamp(timestamp)} + +
+
+ {comment} +
+ + +
+
+
+
+ ); +}; diff --git a/apps/web/components/molecules/commentItem/commentReply.tsx b/apps/web/components/molecules/commentItem/commentReply.tsx new file mode 100644 index 0000000..b65a9fd --- /dev/null +++ b/apps/web/components/molecules/commentItem/commentReply.tsx @@ -0,0 +1,46 @@ +import type { Comment } from '../../../types/types'; +import { ReactComponent as Reply } from '/components/icons/reply.svg'; +import { ReactComponent as Liked } from '/components/icons/liked.svg'; +import { getTimeDiffFromTimestamp } from 'utils/timeDifference/getTimeDiffFromTimestamp'; +import { UserAvatar } from 'molecules/userAvatar/userAvatar'; + +type CommentItemWithReplyProps = Comment; + +export const CommentReply = ({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + id, + author, + timestamp, + comment, +}: CommentItemWithReplyProps) => { + // TODO: Add reply functionality + return ( + <> +
+
+ +
+
+ {author.name} + + {getTimeDiffFromTimestamp(timestamp)} + +
+
+ {comment} +
+ + +
+
+ + ); +}; diff --git a/apps/web/components/molecules/comments/comments.mock.tsx b/apps/web/components/molecules/comments/comments.mock.tsx new file mode 100644 index 0000000..83b2e13 --- /dev/null +++ b/apps/web/components/molecules/comments/comments.mock.tsx @@ -0,0 +1,55 @@ +import type { Comment } from '../../../types/types'; + +export const commentsMock: Comment[] = [ + { + id: '1', + author: { + id: '1', + name: 'Adam Nowak', + avatar_URL: + 'https://images.unsplash.com/photo-1633332755192-727a05c4013d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2960&q=80', + }, + comment: + 'To zadanie frontendowe wydaje się bardzo wymagające, ale na pewno pozwoli na rozwinięcie umiejętności i poszerzenie wiedzy. Trzeba będzie zwrócić uwagę na każdy szczegół, aby stworzyć estetyczny i responsywny interfejs.', + timestamp: 1264938840000, + }, + + { + id: '2', + author: { + id: '2', + name: 'Maria Wójcik', + avatar_URL: + 'https://images.unsplash.com/photo-1509839862600-309617c3201e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80', + }, + comment: + 'Mam nadzieję, że w tym zadaniu będzie można wykorzystać najnowsze technologie i narzędzia, co pozwoli na jeszcze lepsze efekty. Czekam na wyzwania, które pozwolą mi się rozwijać i poszerzać horyzonty.', + timestamp: 1678794840000, + }, + + { + id: '3', + author: { + id: '3', + name: 'Piotr Kowalczyk', + avatar_URL: + 'https://images.unsplash.com/photo-1556157382-97eda2d62296?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80', + }, + comment: + 'Zadanie frontendowe to wspaniała okazja, aby pokazać swoje umiejętności projektowania interfejsów użytkownika. Nie mogę się doczekać, aby zacząć pracę i zobaczyć, co uda mi się stworzyć.', + timestamp: 1672444800000, + }, + + { + id: '4', + author: { + id: '4', + name: 'Karolina Nowakowska', + avatar_URL: + 'https://images.unsplash.com/photo-1607982863027-0cb6818ee8b7?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80', + }, + comment: + 'Zadanie frontendowe może być trudne, ale warto podjąć wyzwanie. Będzie to okazja do nauki nowych technologii i rozwoju umiejętności, co na pewno będzie przydatne w dalszej karierze.', + timestamp: 1678808591856, + }, +]; diff --git a/apps/web/components/molecules/comments/comments.tsx b/apps/web/components/molecules/comments/comments.tsx new file mode 100644 index 0000000..632fa1e --- /dev/null +++ b/apps/web/components/molecules/comments/comments.tsx @@ -0,0 +1,27 @@ +import { Heading } from 'ui'; +import { ProtectedComponent } from 'organisms/protectedComponent/protectedComponent'; +import { AddComment } from 'organisms/addComment/addComment'; +import { CommentItem } from 'molecules/commentItem/commentItem'; +import { commentsMock } from 'molecules/comments/comments.mock'; + +const comments = commentsMock.sort((a, b) => b.timestamp - a.timestamp); + +export const Comments = () => { + return ( +
+
+
+ + Komentarze ({comments.length}) + +
+ + + +
+ {comments.map((comment) => ( + + ))} +
+ ); +}; diff --git a/apps/web/components/molecules/opinionItem/opinionItem.tsx b/apps/web/components/molecules/opinionItem/opinionItem.tsx new file mode 100644 index 0000000..72dad53 --- /dev/null +++ b/apps/web/components/molecules/opinionItem/opinionItem.tsx @@ -0,0 +1,44 @@ +import { Card, Heading, Text } from 'ui'; +import clsx from 'clsx'; +import type { Opinion } from '../../../types/types'; +import { UserAvatar } from 'molecules/userAvatar/userAvatar'; + +type OpinionItemProps = Opinion; + +export const OpinionItem = ({ author, rating, comment }: OpinionItemProps) => { + return ( +
+ +
+ +
+ + {author.name} + +
+ {Array.from({ length: 5 }, (_, i) => { + const starClass = + i < Math.round(rating) ? 'text-yellow-400' : 'text-gray-300'; + return ( + + ); + })} +
+ + {comment} + +
+
+
+
+ ); +}; diff --git a/apps/web/components/molecules/opinions/opinions.mock.tsx b/apps/web/components/molecules/opinions/opinions.mock.tsx new file mode 100644 index 0000000..b344028 --- /dev/null +++ b/apps/web/components/molecules/opinions/opinions.mock.tsx @@ -0,0 +1,40 @@ +import type { Opinion } from '../../../types/types'; + +export const opinions: Opinion[] = [ + { + id: '1', + author: { + id: '1', + name: 'Adam Nowak', + avatar_URL: + 'https://images.unsplash.com/photo-1633332755192-727a05c4013d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2960&q=80', + }, + rating: 5, + comment: + 'Zadanie programistyczne, które otrzymałem, było dobrze sformułowane i precyzyjnie opisywało wymagania dotyczące tworzenia aplikacji internetowej. Zadanie miało na celu zaimplementowanie funkcjonalności wyszukiwania produktów w bazie danych i wyświetlenia wyników na stronie internetowej.', + }, + { + id: '2', + author: { + id: '2', + name: 'Maria Wójcik', + avatar_URL: + 'https://images.unsplash.com/photo-1509839862600-309617c3201e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80', + }, + rating: 5, + comment: + 'Zadanie programistyczne, które otrzymałem, było dobrze sformułowane i precyzyjnie opisywało wymagania dotyczące tworzenia aplikacji internetowej. Zadanie miało na celu zaimplementowanie funkcjonalności wyszukiwania produktów w bazie danych i wyświetlenia wyników na stronie internetowej.', + }, + { + id: '3', + author: { + id: '3', + name: 'Piotr Kowalczyk', + avatar_URL: + 'https://images.unsplash.com/photo-1556157382-97eda2d62296?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80', + }, + rating: 4, + comment: + 'Zadanie programistyczne, które otrzymałem, było dobrze sformułowane i precyzyjnie opisywało wymagania dotyczące tworzenia aplikacji internetowej. Zadanie miało na celu zaimplementowanie funkcjonalności wyszukiwania produktów w bazie danych i wyświetlenia wyników na stronie internetowej.', + }, +]; diff --git a/apps/web/components/molecules/opinions/opinions.tsx b/apps/web/components/molecules/opinions/opinions.tsx new file mode 100644 index 0000000..8916cd4 --- /dev/null +++ b/apps/web/components/molecules/opinions/opinions.tsx @@ -0,0 +1,23 @@ +import { OpinionItem } from 'molecules/opinionItem/opinionItem'; +import { AddOpinion } from 'organisms/addOpinion/addOpinion'; +import { OpinionStats } from 'molecules/opinonStats/opinionStats'; +import { ProtectedComponent } from 'organisms/protectedComponent/protectedComponent'; +import { opinions } from 'molecules/opinions/opinions.mock'; + +export const Opinions = () => { + return ( + <> +
+
+ + + +
+ +
+ {opinions.map((opinion, id) => ( + + ))} + + ); +}; diff --git a/apps/web/components/molecules/opinonStats/opinionStats.tsx b/apps/web/components/molecules/opinonStats/opinionStats.tsx new file mode 100644 index 0000000..2ac6967 --- /dev/null +++ b/apps/web/components/molecules/opinonStats/opinionStats.tsx @@ -0,0 +1,78 @@ +import { Card, Heading } from 'ui'; +import type { Opinion } from '../../../types/types'; +import { opinionPolishPlurals } from 'molecules/opinonStats/opinionStats.utils'; + +type OpinionStatsProps = { + opinions: Opinion[]; +}; + +export const OpinionStats = ({ opinions }: OpinionStatsProps) => { + return ( +
+ +
+ + Statystyki + + +
+ + + + + +

4.95 / 5

+
+

+ {opinionPolishPlurals(opinions.length)} +

+
+
+
+ ); +}; diff --git a/apps/web/components/molecules/opinonStats/opinionStats.utils.tsx b/apps/web/components/molecules/opinonStats/opinionStats.utils.tsx new file mode 100644 index 0000000..b4394e4 --- /dev/null +++ b/apps/web/components/molecules/opinonStats/opinionStats.utils.tsx @@ -0,0 +1,14 @@ +import { polishPlurals } from 'utils/plurals/PolishPlurals'; + +export const opinionPolishPlurals = (value: number) => { + const OpinionsPluralsPosts = { + zero: 'opinii', + one: 'opinia', + two: 'opinie', + few: 'opinie', + many: 'opinii', + other: 'opinie', + }; + + return polishPlurals(OpinionsPluralsPosts, value); +}; diff --git a/apps/web/components/molecules/solutions/solutions.tsx b/apps/web/components/molecules/solutions/solutions.tsx new file mode 100644 index 0000000..5748e06 --- /dev/null +++ b/apps/web/components/molecules/solutions/solutions.tsx @@ -0,0 +1,3 @@ +export const Solutions = () => { + return

Rozwiązania

; +}; diff --git a/apps/web/components/molecules/tabs/tabs.tsx b/apps/web/components/molecules/tabs/tabs.tsx new file mode 100644 index 0000000..0868217 --- /dev/null +++ b/apps/web/components/molecules/tabs/tabs.tsx @@ -0,0 +1,38 @@ +import { Tab } from '@headlessui/react'; +import clsx from 'clsx'; +import { tabs } from 'molecules/tabs/tabsContent'; + +export const Tabs = () => { + return ( +
+ + + {tabs.map((tab) => ( + + clsx( + 'w-full rounded-tl-lg rounded-tr-lg py-2.5 text-sm font-medium text-blue-700', + 'ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 hover:bg-blue-50 transition ease-in-out duration-150', + selected ? 'border-2 border-blue-500 shadow' : 'text-black', + ) + } + > + {tab.label} + + ))} + + + {tabs.map((tab) => { + const Content = tab.content; + return ( + + + + ); + })} + + +
+ ); +}; diff --git a/apps/web/components/molecules/tabs/tabsContent.tsx b/apps/web/components/molecules/tabs/tabsContent.tsx new file mode 100644 index 0000000..a6545a0 --- /dev/null +++ b/apps/web/components/molecules/tabs/tabsContent.tsx @@ -0,0 +1,23 @@ +import { Opinions } from 'molecules/opinions/opinions'; +import { Comments } from 'molecules/comments/comments'; +import { Solutions } from 'molecules/solutions/solutions'; + +type TabObject = { + label: string; + content: () => JSX.Element; +}; + +export const tabs: TabObject[] = [ + { + label: 'Opinie', + content: Opinions, + }, + { + label: 'Rozwiązania', + content: Solutions, + }, + { + label: 'Komentarze', + content: Comments, + }, +]; diff --git a/apps/web/components/molecules/userAvatar/userAvatar.tsx b/apps/web/components/molecules/userAvatar/userAvatar.tsx new file mode 100644 index 0000000..6073ae7 --- /dev/null +++ b/apps/web/components/molecules/userAvatar/userAvatar.tsx @@ -0,0 +1,28 @@ +import Image from 'next/image'; +import clsx from 'clsx'; + +type CommentUserAvatarProps = { + avatar: string; + size?: '12' | '10' | '24'; +}; + +export const UserAvatar = ({ avatar, size = '24' }: CommentUserAvatarProps) => { + const sizes = { + 24: 'w-24 h-24', + 12: 'w-12 h-12', + 10: 'w-10 h-10', + }; + + return ( + + ); +}; diff --git a/apps/web/components/organisms/addComment/addComment.tsx b/apps/web/components/organisms/addComment/addComment.tsx new file mode 100644 index 0000000..1512a23 --- /dev/null +++ b/apps/web/components/organisms/addComment/addComment.tsx @@ -0,0 +1,25 @@ +import { useAddCommentForm } from 'organisms/addComment/useAddCommentForm'; +import { Button, TextArea } from 'ui'; + +export const AddComment = () => { + const { submitHandler, register, errors } = useAddCommentForm(); + + return ( +
+