├── .babelrc.json
├── .dockerignore
├── .eslintcache
├── .eslintrc.js
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.js
├── components
├── ContactUs.js
├── Footer.js
├── about
│ ├── About.js
│ ├── AboutComponent.js
│ ├── AboutComponentRow.js
│ ├── AboutZh.js
│ └── MyImage.js
├── artists
│ ├── DiscoverArtists.js
│ ├── DiscoverAuthorList.js
│ ├── DiscoverCollectionList.js
│ └── ProfileDetailCard.js
├── collection-page
│ ├── CollectionDetails.js
│ ├── DeleteCollectionModal.js
│ └── edit-collection
│ │ ├── EditCollectionContainer.js
│ │ ├── EditCollectionForm.js
│ │ └── EditCollectionModal.js
├── create
│ ├── Create.js
│ ├── CreateContainer.js
│ └── CreateForm.js
├── discover
│ ├── Discover.js
│ └── DiscoverCollectionList.js
├── home
│ ├── Home.js
│ ├── Latest.js
│ └── TypeList.js
├── license
│ ├── License.js
│ └── LicenseZh.js
├── others
│ ├── CategoryBar.js
│ ├── LicenseButton.js
│ ├── NavSearchBar.js
│ ├── PhotoRelatedTagBar.js
│ ├── RelatedTagBar.js
│ ├── StatusButton.js
│ ├── TagBar.js
│ ├── TextInput.js
│ ├── TextInputDescription.js
│ ├── TokenExpireModal.js
│ ├── TypeButton.js
│ ├── UserCollectionsList.js
│ ├── button
│ │ ├── LoadMore.js
│ │ ├── edit-collection-btn
│ │ │ ├── CollectionDropdownButton.js
│ │ │ └── CollectionDropdownButtonDotIcon.js
│ │ └── edit-photo-btn
│ │ │ └── DropdownButton.js
│ ├── photo-card
│ │ ├── CategoryCard.js
│ │ ├── PhotoCard.js
│ │ ├── PhotoMoreDetailsModal.js
│ │ └── SaveToCollectionsModal.js
│ ├── photo-list
│ │ ├── HomePhotoList.js
│ │ └── HomePhotoListContainer.js
│ └── search-bar
│ │ ├── DropdownButton.js
│ │ └── SearchBar.js
├── photo-page
│ ├── Colorbox.js
│ ├── DeletePhotoModal.js
│ ├── PhotoDetails.js
│ ├── PhotoDetailsContainer.js
│ ├── PhotoInfo.js
│ └── RelatedPhotos.js
├── profile
│ ├── Profile.js
│ ├── Settings.js
│ ├── UserArtworks.js
│ ├── UserCollections.js
│ ├── UserLikes.js
│ ├── UserPage.js
│ ├── UserPhotos.js
│ ├── change-password
│ │ ├── ChangePassword.js
│ │ ├── ChangePasswordContainer.js
│ │ └── ChangePasswordForm.js
│ ├── delete-account
│ │ ├── DeleteAccount.js
│ │ └── DeleteAccountModal.js
│ ├── edit-profile
│ │ ├── EditProfile.js
│ │ ├── EditProfileContainer.js
│ │ └── EditProfileForm.js
│ ├── update-avatar
│ │ ├── AvatarEdit.js
│ │ └── UpdateAvatar.js
│ └── user-collections
│ │ ├── CollectionCard.js
│ │ ├── DeleteCollectionModal.js
│ │ └── edit-collection
│ │ ├── EditCollectionContainer.js
│ │ ├── EditCollectionForm.js
│ │ └── EditCollectionModal.js
├── search
│ ├── SearchPage.js
│ ├── SearchPagePhotoList.js
│ ├── SearchPagePhotoListContainer.js
│ └── SearchPhotoCard.js
├── sign-in
│ ├── SignIn.js
│ ├── SignInContainer.js
│ └── SignInForm.js
├── sign-up
│ ├── SignUp.js
│ ├── SignUpContainer.js
│ └── SignUpForm.js
└── upload
│ └── uploadComponent.js
├── config.js
├── graphql
├── fragment.js
├── mutations.js
└── queries.js
├── hooks
├── useAuthorizedUser.js
├── useChangePassword.js
├── useCollectPhoto.js
├── useCollection.js
├── useCollections.js
├── useCreateAndLikePhoto.js
├── useCreateCollection.js
├── useCreateCollectionAndCollectPhoto.js
├── useCreatePhoto.js
├── useDeleteCollection.js
├── useDeletePhoto.js
├── useDeleteUser.js
├── useDiscoverCollections.js
├── useDownloadPhoto.js
├── useEditCollection.js
├── useEditPhotoLabels.js
├── useField.js
├── useFollowUser.js
├── useLikePhoto.js
├── usePhoto.js
├── usePhotos.js
├── useSignIn.js
├── useSignUp.js
├── useUncollectPhoto.js
├── useUnfollowUser.js
├── useUnlikeAndDeletePhoto.js
├── useUnlikePhoto.js
├── useUpdateAvatar.js
├── useUpdateProfile.js
├── useUser.js
├── useUserCollectionsPlus.js
├── useUserLikes.js
├── useUsers.js
└── useVerifyMetadata.js
├── img
├── aboutImg4.jpg
├── galleryIcon.jpg
├── logo
│ ├── logo.svg
│ ├── logo1.svg
│ └── logo2.svg
├── overlays
│ ├── 01.png
│ ├── 02.png
│ ├── 03.png
│ ├── 04.png
│ ├── 05.png
│ ├── 06.png
│ ├── 07.png
│ ├── 08.png
│ └── 09.png
└── svg
│ ├── arrow_left.svg
│ └── arrow_right.svg
├── index.css
├── index.js
├── logo.png
├── mdb.css
├── scss
├── about
│ └── about.scss
├── collection-list
│ └── collection-list.scss
├── create
│ └── create.scss
├── home
│ └── home.scss
├── license
│ └── license.scss
├── navbar
│ └── navbar.scss
├── photo-card
│ └── photo-card.scss
├── photo-details
│ └── photo-details.scss
├── photo-list
│ └── photo-list.scss
├── profile
│ └── profile.scss
└── style.scss
├── styles.css
├── theme.js
└── utils
├── formatters.js
├── photos.json
└── saveToS3.js
/.babelrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-react", "@babel/preset-env"],
3 | "plugins": ["@emotion"]
4 | }
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2020: true,
5 | jest: true,
6 | },
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:react/recommended',
10 | 'airbnb',
11 | ],
12 | parser: 'babel-eslint',
13 | parserOptions: {
14 | ecmaFeatures: {
15 | jsx: true,
16 | },
17 | ecmaVersion: 12,
18 | sourceType: 'module',
19 | },
20 | plugins: [
21 | 'react',
22 | ],
23 | rules: {
24 | 'react/prop-types': 'off',
25 | semi: 'error',
26 | 'linebreak-style': 0,
27 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
28 | 'react/jsx-props-no-spreading': 'off',
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 | .eslintcache
22 |
23 | package-lock.json
24 |
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
29 | /src/*css.map
30 | /src/css
31 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14
2 |
3 | # Create app directory
4 | WORKDIR /usr/src/app
5 |
6 | # Install app dependencies
7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied
8 | # where available (npm@5+)
9 | COPY package*.json ./
10 |
11 | RUN npm install
12 | # If you are building your code for production
13 | # RUN npm ci --only=production
14 |
15 | # Bundle app source
16 | COPY . .
17 |
18 | EXPOSE 3000
19 | CMD [ "npm", "run", "start" ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Philo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Philo Art
2 |
3 | The relative Node.js backend is here: [PhiloArt-Backend](https://github.com/Philo-Li/philo-art-backend)
4 |
5 | ## ✔️ Website
6 |
7 | https://philoart.io/
8 |
9 | ## 🚀 PhiloArt - Made for creators!
10 |
11 | - Personal ArtWork Gallery
12 | - Outstanding personal artwork site + high quality gallery
13 | - Help you publish and manage your work more easily
14 | - and faster access to the best free copyrighted images
15 |
16 | - What you can do with PhiloArt?
17 | - Upload, manage and publish your work with rich protocols
18 | - Discover and follow favorite artists and get instant updates
19 | - Discover, search and download high quality free copyrighted images for free
20 | - Like your favorite images and curate your own collections
21 |
22 | - Who needs PhiloArt?
23 | - Artist
24 | - Photographer
25 | - Creator
26 | - Designer
27 | - And you
28 |
29 | - Why choose PhiloArt?
30 | - Easily and quickly publish and manage your work with multiple protocols
31 | - Discover best works and free copyrighted works
32 | - Follow favorite artists
33 | - Encourage creativity, inspire and enhance creativity
34 |
35 | ## 🐛 Found a bug?
36 |
37 | Submit an issue with the bug description and a way to reproduce the bug. If you have already come up with a solution, we will gladly accept a pull request.
38 |
39 | If you have any questions or suggestions that might make the Philo Art experience even better, please let us know! You can get in touch with us at philoart42@gmail.com.
40 |
41 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 |
5 | philoart:
6 | # image: nodejs:v1.0
7 | build:
8 | context: .
9 | dockerfile: Dockerfile
10 | container_name: philoart
11 | volumes:
12 | - '.:/app'
13 | ports:
14 | - 3000:3000
15 | environment:
16 | - CHOKIDAR_USEPOLLING=true
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "philoart",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@ant-design/icons": "^4.7.0",
7 | "@apollo/client": "^3.2.5",
8 | "@apollo/react-hooks": "^4.0.0",
9 | "@emotion/react": "^11.1.5",
10 | "@material-ui/core": "^4.11.3",
11 | "@material-ui/icons": "^4.11.2",
12 | "@testing-library/jest-dom": "^5.11.6",
13 | "@testing-library/react": "^11.2.2",
14 | "@testing-library/user-event": "^12.3.0",
15 | "antd": "^4.18.6",
16 | "apollo-link-context": "^1.0.20",
17 | "aws-sdk": "^2.1069.0",
18 | "axios": "^0.21.1",
19 | "bootstrap": "^5.1.1",
20 | "bootstrap-icons": "^1.8.1",
21 | "cross-env": "^7.0.3",
22 | "date-fns": "^2.16.1",
23 | "dotenv": "^8.2.0",
24 | "formik": "^2.2.6",
25 | "glamor": "^2.17.9",
26 | "got": "^11.8.1",
27 | "graphql": "^15.3.0",
28 | "http2": "^3.3.7",
29 | "mdbreact": "^4.11.1",
30 | "nanoid": "^3.3.0",
31 | "path-browserify": "^1.0.1",
32 | "pexels": "^1.0.1",
33 | "query-string": "^6.14.1",
34 | "react": "^17.0.2",
35 | "react-avatar-edit": "^1.1.0",
36 | "react-blockies": "^1.4.1",
37 | "react-bootstrap": "^2.0.0-rc.0",
38 | "react-dom": "^17.0.2",
39 | "react-dropzone": "^12.0.0",
40 | "react-easy-crop": "^3.3.1",
41 | "react-icons": "^4.1.0",
42 | "react-inner-image-zoom": "^1.3.0",
43 | "react-lazyload": "^3.2.0",
44 | "react-map-interaction": "^2.1.0",
45 | "react-masonry-css": "^1.0.14",
46 | "react-router-dom": "^5.2.0",
47 | "react-scripts": "^4.0.3",
48 | "react-select": "^3.2.0",
49 | "react-spinners": "^0.10.6",
50 | "react-youtube-embed": "^1.0.3",
51 | "semantic-ui-react": "^2.0.3",
52 | "subscriptions-transport-ws": "^0.9.18",
53 | "web-vitals": "^0.2.4",
54 | "yup": "^0.32.9"
55 | },
56 | "scripts": {
57 | "start": "react-scripts start",
58 | "build": "react-scripts build",
59 | "test": "react-scripts test",
60 | "eject": "react-scripts eject",
61 | "keep": "serve -l 3000 -s build"
62 | },
63 | "eslintConfig": {
64 | "extends": [
65 | "react-app",
66 | "react-app/jest"
67 | ]
68 | },
69 | "browserslist": {
70 | "production": [
71 | ">0.2%",
72 | "not dead",
73 | "not op_mini all"
74 | ],
75 | "development": [
76 | "last 1 chrome version",
77 | "last 1 firefox version",
78 | "last 1 safari version"
79 | ]
80 | },
81 | "proxy": "http://localhost:5005",
82 | "devDependencies": {
83 | "@typescript-eslint/parser": "^4.10.0",
84 | "eslint": "^7.14.0",
85 | "eslint-config-airbnb": "^18.2.1",
86 | "typescript": "^4.1.3"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
22 |
28 |
29 |
30 |
31 |
32 |
36 |
37 |
41 |
42 |
51 | Philo Art
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/components/ContactUs.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React from 'react';
3 |
4 | const ContactUs = () => (
5 |
6 |
7 |
8 |
Contact Us
9 |
10 |
11 |
12 | If you have any questions or suggestions that might make the PhiloArt experience even better, please let us know! You can get in touch with us at philoart42@gmail.com.
13 |
14 |
15 |
16 |
17 | );
18 |
19 | export default ContactUs;
20 |
--------------------------------------------------------------------------------
/src/components/about/AboutComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Col } from 'react-bootstrap';
3 | import MyImage from './MyImage';
4 |
5 | const AboutComponent = ({ msgToShow }) => {
6 | if (!msgToShow) return null;
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
{msgToShow.title}
15 |
16 | {msgToShow.msgList.map((item) => (
17 |
18 |
19 |
20 |
21 | {item.msg}
22 |
23 |
24 |
25 | ))}
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default AboutComponent;
34 |
--------------------------------------------------------------------------------
/src/components/about/AboutComponentRow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Col } from 'react-bootstrap';
3 | import MyImage from './MyImage';
4 |
5 | const AboutComponentRow = ({ msgToShow }) => {
6 | if (!msgToShow) return null;
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
{msgToShow.title}
15 |
16 | {msgToShow.msgList.map((item) => (
17 |
18 |
19 |
20 |
21 |
22 |
23 | {item.msg}
24 |
25 |
26 |
27 | ))}
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default AboutComponentRow;
36 |
--------------------------------------------------------------------------------
/src/components/about/AboutZh.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React from 'react';
3 | import { Card } from 'react-bootstrap';
4 | import aboutImg4 from '../../img/aboutImg4.jpg';
5 | import AboutComponent from './AboutComponent';
6 | import AboutComponentRow from './AboutComponentRow';
7 | import MyImage from './MyImage';
8 |
9 | const img1 = 'https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1416&q=80';
10 | const img2 = 'https://images.unsplash.com/photo-1497030947858-3f40f1508e84?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1500&q=80';
11 | const img3 = 'https://images.unsplash.com/3/doctype-hi-res.jpg?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=1492&q=80';
12 | const img4 = aboutImg4;
13 |
14 | const AboutZh = () => {
15 | const msgToShow = [
16 | {
17 | title: '出色的个人艺术作品站+高质量图库',
18 | subtitle1: '帮助你更便捷地发布和管理自己的作品',
19 | subtitle2: '以及更快地获取最优秀的免费版权图片',
20 | intro: 'PhiloArt - 为创作者而生!',
21 | imgFirst: true,
22 | img: { srcTiny: img1, srcSmall: img1, srcLarge: img1 },
23 | },
24 | {
25 | title: 'PhiloArt 可以做什么?',
26 | imgFirst: false,
27 | img: { srcTiny: img2, srcSmall: img2, srcLarge: img2 },
28 | msgList: [
29 | { icon: 'bi bi-download icon-check', msg: '上传、管理并且以丰富的协议发布你的作品' },
30 | { icon: 'bi bi-search icon-check', msg: '发现和关注喜欢的艺术家,获取即时动态' },
31 | { icon: 'bi bi-heart icon-check', msg: '发现、搜索和免费下载高品质免费版权图片' },
32 | { icon: 'bi bi-plus-square icon-check', msg: '点赞你喜欢的图片并创建专属收藏夹' },
33 | ],
34 | },
35 | {
36 | title: '谁需要 PhiloArt?',
37 | imgFirst: true,
38 | img: { srcTiny: img3, srcSmall: img3, srcLarge: img3 },
39 | msgList: [
40 | { icon: 'bi bi-palette icon-check', msg: '艺术家' },
41 | { icon: 'bi bi-camera icon-check', msg: '摄影师' },
42 | { icon: 'bi bi-pencil icon-check', msg: '创作者' },
43 | { icon: 'bi bi-shop icon-check', msg: '设计师' },
44 | { icon: 'bi bi-emoji-sunglasses icon-check', msg: '...还有你' },
45 | ],
46 | },
47 | {
48 | title: '为什么选择 PhiloArt?',
49 | imgFirst: false,
50 | img: { srcTiny: img4, srcSmall: img4, srcLarge: img4 },
51 | msgList: [
52 | { icon: 'bi bi-check2 icon-check', msg: '方便快捷地以多种协议发布和管理你的作品' },
53 | { icon: 'bi bi-check2 icon-check', msg: '发现更多优秀的作品及免费版权作品' },
54 | { icon: 'bi bi-check2 icon-check', msg: '关注喜欢的艺术家' },
55 | { icon: 'bi bi-check2 icon-check', msg: '鼓励创作、激发灵感和提升创造力' },
56 | ],
57 | },
58 | ];
59 | return (
60 |
61 |
62 |
63 |
关于 PhiloArt
64 |
65 |
English
66 |
67 |
68 |
{msgToShow[0].intro}
69 |
70 |
71 |
{msgToShow[0].title}
72 |
73 | {msgToShow[0].subtitle1}
74 |
75 |
76 | {msgToShow[0].subtitle2}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | 如果你有任何疑问或帮助 PhiloArt 实现更好的体验的改进建议,可以发送邮件到 philoart42@gmail.com.
89 |
90 |
91 |
92 | {/* 这世界战争不停,争吵不息,满目疮痍。我想画一点温暖东西,治愈自己,也治愈他人。 */}
93 |
94 | );
95 | };
96 |
97 | export default AboutZh;
98 |
--------------------------------------------------------------------------------
/src/components/about/MyImage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const MyImage = ({ image }) => {
4 | if (!image) return null;
5 | return (
6 |
7 |
8 |
9 |
15 |
16 | );
17 | };
18 |
19 | export default MyImage;
20 |
--------------------------------------------------------------------------------
/src/components/artists/DiscoverArtists.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { css } from '@emotion/react';
3 | import PacmanLoader from 'react-spinners/PacmanLoader';
4 | import useUsers from '../../hooks/useUsers';
5 | import DiscoverAuthorList from './DiscoverAuthorList';
6 |
7 | const override = css`
8 | display: flex;
9 | justify-content: center;
10 | align-item: center;
11 | margin: 3rem;
12 | margin-bottom: 6rem;
13 | `;
14 |
15 | const DiscoverArtists = () => {
16 | const [allUsers, setAllUsers] = useState();
17 | const [loading, setLoading] = useState(false);
18 | const { users, fetchMore, hasNextPage } = useUsers({
19 | first: 30,
20 | });
21 |
22 | useEffect(() => {
23 | if (users) {
24 | const temp = users && users.edges
25 | ? users.edges.map((edge) => edge.node)
26 | : [];
27 |
28 | setAllUsers(temp);
29 | setLoading(false);
30 | }
31 | }, [users]);
32 |
33 | const clickFetchMore = () => {
34 | fetchMore();
35 | setLoading(true);
36 | };
37 |
38 | if (allUsers === undefined) {
39 | return (
40 |
50 | );
51 | }
52 |
53 | return (
54 |
55 |
56 |
57 |
Discover Artists
58 |
59 |
60 |
61 |
67 |
68 |
69 | );
70 | };
71 |
72 | export default DiscoverArtists;
73 |
--------------------------------------------------------------------------------
/src/components/artists/DiscoverAuthorList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card } from 'react-bootstrap';
3 | import { nanoid } from 'nanoid';
4 | import Masonry from 'react-masonry-css';
5 | import LoadMore from '../others/button/LoadMore';
6 | import ProfileDetailCard from './ProfileDetailCard';
7 |
8 | const breakpointColumnsObj = {
9 | default: 3,
10 | 800: 2,
11 | 500: 1,
12 | };
13 |
14 | const DiscoverAuthorList = ({
15 | allUsers, clickFetchMore, loading, hasNextPage,
16 | }) => {
17 | if (!allUsers) {
18 | return (
19 |
20 |
No result
21 |
22 | );
23 | }
24 |
25 | const initProfileImage = 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png';
26 |
27 | return (
28 |
29 |
34 | {allUsers.map((user) => (
35 |
36 |
37 |
43 |
44 |
45 | ))}
46 |
47 |
52 |
53 | );
54 | };
55 |
56 | export default DiscoverAuthorList;
57 |
--------------------------------------------------------------------------------
/src/components/artists/DiscoverCollectionList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // import { Card } from 'react-bootstrap';
3 | import { useHistory } from 'react-router-dom';
4 | import Masonry from 'react-masonry-css';
5 | import CollectionCard from '../profile/user-collections/CollectionCard';
6 | // import galleryIcon from '../../img/galleryIcon.jpg';
7 |
8 | const breakpointColumnsObj = {
9 | default: 3,
10 | 800: 2,
11 | 500: 1,
12 | };
13 |
14 | // const INIT_COVER = galleryIcon;
15 |
16 | const DiscoverCollectionList = ({ allCollections, category }) => {
17 | const collectionsToShow = allCollections
18 | .filter((collection) => collection.description === category);
19 |
20 | const history = useHistory();
21 | if (!collectionsToShow) {
22 | return (
23 |
24 |
No result
25 |
26 | );
27 | }
28 |
29 | // eslint-disable-next-line no-unused-vars
30 | const openCollection = (collection) => {
31 | history.push(`/collection/${collection.id}`);
32 | };
33 |
34 | return (
35 |
36 |
41 | {/* {collectionsToShow.map((collection) => (
42 |
43 | { openCollection(collection); }}
46 | onKeyPress={() => openCollection(collection)}
47 | role="button"
48 | tabIndex="0"
49 | >
50 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {collection.title}
62 | (
63 | {collection.photoCount}
64 | )
65 |
66 |
67 |
68 | ))} */}
69 | {collectionsToShow.map((collection) => (
70 |
75 | ))}
76 |
77 |
78 | );
79 | };
80 |
81 | export default DiscoverCollectionList;
82 |
--------------------------------------------------------------------------------
/src/components/artists/ProfileDetailCard.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable arrow-body-style */
2 | import React from 'react';
3 | import { Image } from 'react-bootstrap';
4 |
5 | const ProfileDetailCard = ({ profileImage, userNow }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
{userNow && `${userNow.firstName} ${userNow.lastName || ''}`}
14 |
15 |
16 | {userNow && userNow.description && (
17 |
18 |
{userNow.description}
19 |
20 | )}
21 |
22 |
23 | {`${userNow ? userNow.photoCount : 0} artworks`}
24 |
25 | {/*
26 | {`${userNow ? userNow.followingCount : 0} followings`}
27 |
*/}
28 |
29 | {`${userNow ? userNow.followerCount : 0} followers`}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default ProfileDetailCard;
37 |
--------------------------------------------------------------------------------
/src/components/collection-page/CollectionDetails.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React, { useEffect, useState } from 'react';
3 | import { useParams } from 'react-router-dom';
4 | import { css } from '@emotion/react';
5 | import PacmanLoader from 'react-spinners/PacmanLoader';
6 | import CollectionDropdownButton from '../others/button/edit-collection-btn/CollectionDropdownButton';
7 | import HomePhotoList from '../others/photo-list/HomePhotoList';
8 | import useCollection from '../../hooks/useCollection';
9 | import EditCollectionModal from './edit-collection/EditCollectionModal';
10 | import DeleteCollectionModal from './DeleteCollectionModal';
11 |
12 | const override = css`
13 | display: flex;
14 | justify-content: center;
15 | align-item: center;
16 | margin: 3rem;
17 | margin-bottom: 6rem;
18 | `;
19 |
20 | const CollectionDetails = () => {
21 | const { id } = useParams();
22 | const [allPhotos, setAllPhotos] = useState();
23 | const [collectionNow, setCollectionNow] = useState();
24 | const [showEditCollectionModal, setShowEditCollectionModal] = useState(false);
25 | const [showDeleteModal, setShowDeleteModal] = useState(false);
26 | const [loading, setLoading] = useState(false);
27 | const userId = localStorage.getItem('userId');
28 | const username = localStorage.getItem('username');
29 |
30 | const variables = {
31 | id,
32 | checkUserLike: userId,
33 | first: 20,
34 | };
35 |
36 | const { collection, fetchMore, hasNextPage } = useCollection(variables);
37 |
38 | useEffect(() => {
39 | if (collection) {
40 | const temp = collection.photoCount > 0
41 | ? collection.photos.edges.map((edge) => edge.node.photo)
42 | : [];
43 | setCollectionNow(collection);
44 |
45 | setAllPhotos(temp);
46 | setLoading(false);
47 | }
48 | }, [collection]);
49 |
50 | const clickFetchMore = () => {
51 | if (collectionNow.photoCount > allPhotos.length) {
52 | fetchMore();
53 | setLoading(true);
54 | }
55 | };
56 |
57 | if (!collectionNow) {
58 | return (
59 |
62 | );
63 | }
64 |
65 | return (
66 |
67 |
68 |
69 |
70 |
71 | {collectionNow.title}
72 |
73 |
74 |
75 |
76 | Collected by
77 | {' '}
78 | {collectionNow.user.username}
79 |
80 |
81 |
82 |
83 | {collectionNow.photoCount}
84 | {' '}
85 | photos
86 |
87 |
88 |
89 |
90 |
91 |
92 | {username && collectionNow.user.username === username && (
93 |
97 | )}
98 |
99 |
100 |
106 |
111 |
119 |
120 | );
121 | };
122 |
123 | export default CollectionDetails;
124 |
--------------------------------------------------------------------------------
/src/components/collection-page/DeleteCollectionModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useHistory } from 'react-router-dom';
3 | import { Modal, Alert } from 'react-bootstrap';
4 | import useDeleteCollection from '../../hooks/useDeleteCollection';
5 |
6 | const DeleteCollectionModal = ({
7 | collectionNow,
8 | showDeleteModal,
9 | setShowDeleteModal,
10 | }) => {
11 | const [errorInfo, setErrorInfo] = useState('');
12 | const [successInfo, setSuccessInfo] = useState('');
13 | const [deleteCollection] = useDeleteCollection();
14 | const history = useHistory();
15 |
16 | const deleteSingleCollection = async () => {
17 | try {
18 | await deleteCollection({ id: collectionNow.id });
19 | setSuccessInfo('Collection is deleted');
20 | setTimeout(() => { setSuccessInfo(''); setShowDeleteModal(false); history.goBack(); }, 3000);
21 | } catch (e) {
22 | setErrorInfo(e.message);
23 | setTimeout(() => { setErrorInfo(''); }, 3000);
24 | }
25 | };
26 |
27 | return (
28 |
29 |
setShowDeleteModal(false)}
32 | centered
33 | dialogClassName="modal-90w"
34 | aria-labelledby="example-custom-modal-styling-title"
35 | >
36 |
37 |
38 | Delete this collection?
39 |
40 |
41 | {errorInfo && (
42 |
43 | {errorInfo}
44 |
45 | )}
46 | {successInfo && (
47 |
48 | {successInfo}
49 |
50 | )}
51 |
52 | setShowDeleteModal(false)}>
53 | Close
54 |
55 | deleteSingleCollection()}>
56 |
57 | Delete
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default DeleteCollectionModal;
66 |
--------------------------------------------------------------------------------
/src/components/collection-page/edit-collection/EditCollectionContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik } from 'formik';
3 | import { Alert } from 'react-bootstrap';
4 | import * as Yup from 'yup';
5 |
6 | import EditCollectionForm from './EditCollectionForm';
7 |
8 | const validationSchema = Yup.object().shape({
9 | title: Yup
10 | .string()
11 | .required('Title is required'),
12 | description: Yup
13 | .string()
14 | .max(50, 'Must be 50 characters or less'),
15 | });
16 |
17 | const EditCollectionContainer = ({
18 | initialValues, onSubmit, errorInfo, successInfo, loading,
19 | }) => (
20 |
21 | {errorInfo && (
22 |
23 | {errorInfo}
24 |
25 | )}
26 | {successInfo && (
27 |
28 | {successInfo}
29 |
30 | )}
31 |
36 | {({ handleSubmit }) => }
37 |
38 |
39 | );
40 |
41 | export default EditCollectionContainer;
42 |
--------------------------------------------------------------------------------
/src/components/collection-page/edit-collection/EditCollectionForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button, Spinner } from 'react-bootstrap';
4 | import { Form } from 'formik';
5 |
6 | import TextInput from '../../others/TextInput';
7 |
8 | const EditCollectionForm = ({ loading }) => (
9 |
48 | );
49 |
50 | export default EditCollectionForm;
51 |
--------------------------------------------------------------------------------
/src/components/collection-page/edit-collection/EditCollectionModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Modal } from 'react-bootstrap';
3 | import useEditCollection from '../../../hooks/useEditCollection';
4 | import EditCollectionContainer from './EditCollectionContainer';
5 |
6 | const EditCollectionModal = ({
7 | collectionNow,
8 | setCollectionNow,
9 | showEditCollectionModal,
10 | setShowEditCollectionModal,
11 | }) => {
12 | const [editCollection] = useEditCollection();
13 | const [loading, setLoading] = useState(false);
14 | const [errorInfo, setErrorInfo] = useState('');
15 | const [successInfo, setSuccessInfo] = useState('');
16 |
17 | const initialValues = {
18 | title: collectionNow.title,
19 | description: collectionNow.description || '',
20 | };
21 |
22 | const onSubmit = async (values) => {
23 | const variables = {
24 | collectionId: collectionNow.id,
25 | newTitle: values.title,
26 | newDescription: values.description,
27 | };
28 |
29 | const updatedCollectionNow = {
30 | ...collectionNow,
31 | title: values.title,
32 | description: values.description,
33 | };
34 | setLoading(true);
35 |
36 | try {
37 | await editCollection(variables);
38 | setCollectionNow(updatedCollectionNow);
39 | setSuccessInfo('Collection details updated');
40 | setTimeout(() => { setSuccessInfo(''); }, 3000);
41 | } catch (e) {
42 | setErrorInfo(e.message);
43 | setTimeout(() => { setErrorInfo(''); }, 3000);
44 | }
45 | setLoading(false);
46 | };
47 |
48 | return (
49 |
50 | setShowEditCollectionModal(false)}
53 | centered
54 | dialogClassName="modal-90w"
55 | aria-labelledby="example-custom-modal-styling-title"
56 | >
57 |
58 |
59 | Edit Collection
60 | {' '}
61 | {collectionNow.title}
62 |
63 |
64 |
65 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default EditCollectionModal;
79 |
--------------------------------------------------------------------------------
/src/components/create/Create.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import React, { useState } from 'react';
3 | import axios from 'axios';
4 | import { css } from '@emotion/react';
5 | import { nanoid } from 'nanoid';
6 | import { useHistory } from 'react-router-dom';
7 | import PacmanLoader from 'react-spinners/PacmanLoader';
8 | import CreateContainer from './CreateContainer';
9 | import useCreatePhoto from '../../hooks/useCreatePhoto';
10 | import saveToS3 from '../../utils/saveToS3';
11 | import config from '../../config';
12 |
13 | const override = css`
14 | display: flex;
15 | justify-content: center;
16 | align-item: center;
17 | margin: 3rem;
18 | margin-bottom: 6rem;
19 | `;
20 |
21 | const baseUrl = config.philoartApi;
22 |
23 | const initialValues = {
24 | title: 'Untitled',
25 | description: '',
26 | license: 'CC BY-NC',
27 | type: 'painting',
28 | };
29 |
30 | const Create = () => {
31 | const history = useHistory();
32 | const [successInfo, setSuccessInfo] = useState('');
33 | const [errorInfo, setErrorInfo] = useState('');
34 | const [loading, setLoading] = useState(false);
35 | const [createPhoto] = useCreatePhoto();
36 | const [files, setFiles] = useState([]);
37 | const [license, setLicense] = useState('CC BY-NC');
38 | const [type, setType] = useState('Photograph');
39 | const [status, setStatus] = useState('None');
40 | const [checked, setChecked] = useState(true);
41 | const userId = localStorage.getItem('userId');
42 | const username = localStorage.getItem('username');
43 |
44 | if (!userId) {
45 | return (
46 |
49 | );
50 | }
51 |
52 | const onSubmit = async (values) => {
53 | const {
54 | title, description,
55 | } = values;
56 |
57 | setLoading(true);
58 | try {
59 | const imageKey = `${userId}-${nanoid(7)}`;
60 | const imageUrl = await saveToS3(imageKey, files[0]);
61 |
62 | // store the image data to the server
63 | const variables = {
64 | photoId: imageKey,
65 | title,
66 | year: new Date().getFullYear(),
67 | description,
68 | imageUrl,
69 | license,
70 | type,
71 | status,
72 | allowDownload: checked,
73 | };
74 | await createPhoto(variables);
75 | setLoading(false);
76 | setSuccessInfo('Photo is uploaded successfully!');
77 | setTimeout(() => { setSuccessInfo(''); history.push(`/${username}`); }, 3000);
78 | } catch (e) {
79 | setErrorInfo(e.message);
80 | setLoading(false);
81 | setTimeout(() => { setErrorInfo(''); }, 3000);
82 | }
83 | };
84 |
85 | const handleCheckboxChange = () => {
86 | setChecked(!checked);
87 | };
88 |
89 | return (
90 |
91 |
104 |
105 | );
106 | };
107 |
108 | export default Create;
109 |
--------------------------------------------------------------------------------
/src/components/create/CreateContainer.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React from 'react';
3 | import { Formik } from 'formik';
4 | import { Image, Alert } from 'react-bootstrap';
5 | import * as yup from 'yup';
6 |
7 | import CreateForm from './CreateForm';
8 |
9 | import logo from '../../img/logo/logo2.svg';
10 |
11 | const validationSchema = yup.object().shape({
12 | title: yup
13 | .string()
14 | .required(),
15 | description: yup
16 | .string(),
17 | });
18 |
19 | const CreateContainer = ({
20 | initialValues, onSubmit, successInfo, errorInfo, loading, files, setFiles, setLicense, setType, setStatus, handleCheckboxChange, checked,
21 | }) => (
22 |
23 |
24 |
25 |
Create
26 |
27 |
28 |
29 |
30 |
31 | {errorInfo && (
32 |
33 | {errorInfo}
34 |
35 | )}
36 |
41 | {({ handleSubmit }) => (
42 |
53 | )}
54 |
55 | {successInfo && (
56 |
57 | {successInfo}
58 |
59 | )}
60 |
61 | );
62 |
63 | export default CreateContainer;
64 |
--------------------------------------------------------------------------------
/src/components/create/CreateForm.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/label-has-associated-control */
2 | import React from 'react';
3 | import { Button, Spinner } from 'react-bootstrap';
4 | import { Form } from 'formik';
5 | import TextInput from '../others/TextInput';
6 | import Previews from '../upload/uploadComponent';
7 | import LicenseButton from '../others/LicenseButton';
8 | import TypeButton from '../others/TypeButton';
9 | import StatusButton from '../others/StatusButton';
10 | import TextInputDescription from '../others/TextInputDescription';
11 |
12 | const CreateForm = ({
13 | loading, files, setFiles, setLicense, setType, setStatus, handleCheckboxChange, checked,
14 | }) => (
15 |
103 | );
104 |
105 | export default CreateForm;
106 |
--------------------------------------------------------------------------------
/src/components/discover/Discover.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { css } from '@emotion/react';
3 | import PacmanLoader from 'react-spinners/PacmanLoader';
4 | import DiscoverCollectionList from './DiscoverCollectionList';
5 | import config from '../../config';
6 | import useDiscoverCollections from '../../hooks/useDiscoverCollections';
7 |
8 | const override = css`
9 | display: flex;
10 | justify-content: center;
11 | align-item: center;
12 | margin: 3rem;
13 | margin-bottom: 6rem;
14 | `;
15 |
16 | // const CATEGORY = ['mood', 'animals', 'light', 'nature', 'human', 'road', 'food'];
17 |
18 | const Discover = () => {
19 | const [allCollections, setAllCollections] = useState();
20 | const { collections } = useDiscoverCollections({
21 | username: config.pickyAdmin,
22 | first: 30,
23 | });
24 |
25 | useEffect(() => {
26 | if (collections) {
27 | const temp = collections && collections.edges
28 | ? collections.edges.map((edge) => edge.node)
29 | : [];
30 |
31 | setAllCollections(temp);
32 | }
33 | }, [collections]);
34 |
35 | if (allCollections === undefined) {
36 | return (
37 |
47 | );
48 | }
49 |
50 | return (
51 |
52 |
57 |
63 |
69 |
75 |
81 |
87 |
88 |
89 |
Light and shadow
90 |
91 |
92 |
93 |
99 |
105 |
106 | );
107 | };
108 |
109 | export default Discover;
110 |
--------------------------------------------------------------------------------
/src/components/discover/DiscoverCollectionList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // import { Card } from 'react-bootstrap';
3 | import { useHistory } from 'react-router-dom';
4 | import Masonry from 'react-masonry-css';
5 | import CollectionCard from '../profile/user-collections/CollectionCard';
6 | // import galleryIcon from '../../img/galleryIcon.jpg';
7 |
8 | const breakpointColumnsObj = {
9 | default: 3,
10 | 800: 2,
11 | 500: 1,
12 | };
13 |
14 | // const INIT_COVER = galleryIcon;
15 |
16 | const DiscoverCollectionList = ({ allCollections, category }) => {
17 | const collectionsToShow = allCollections
18 | .filter((collection) => collection.description === category);
19 |
20 | const history = useHistory();
21 | if (!collectionsToShow) {
22 | return (
23 |
24 |
No result
25 |
26 | );
27 | }
28 |
29 | // eslint-disable-next-line no-unused-vars
30 | const openCollection = (collection) => {
31 | history.push(`/collection/${collection.id}`);
32 | };
33 |
34 | return (
35 |
36 |
41 | {/* {collectionsToShow.map((collection) => (
42 |
43 | { openCollection(collection); }}
46 | onKeyPress={() => openCollection(collection)}
47 | role="button"
48 | tabIndex="0"
49 | >
50 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {collection.title}
62 | (
63 | {collection.photoCount}
64 | )
65 |
66 |
67 |
68 | ))} */}
69 | {collectionsToShow.map((collection) => (
70 |
75 | ))}
76 |
77 |
78 | );
79 | };
80 |
81 | export default DiscoverCollectionList;
82 |
--------------------------------------------------------------------------------
/src/components/home/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Tabs, Tab, Carousel } from 'react-bootstrap';
3 | import Latest from './Latest';
4 | import TypeList from './TypeList';
5 | import CategoryBar from '../others/CategoryBar';
6 | import Discover from '../discover/Discover';
7 | import SearchBar from '../others/search-bar/SearchBar';
8 |
9 | const Home = () => {
10 | const [key, setKey] = useState('freetouse');
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Share your artworks with the world.
21 | Create, and Post it
22 |
23 |
24 |
25 |
26 |
27 | Create, Mint, and Sell
28 | Discover the best NFTs(Upcoming).
29 |
30 |
31 |
32 |
33 |
34 | Discover the best artworks.
35 | Free for personal use and download.
36 |
37 |
38 |
39 |
40 |
41 |
setKey(k)}
45 | className="mb-3"
46 | >
47 | {/*
48 |
55 | */}
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | export default Home;
83 |
--------------------------------------------------------------------------------
/src/components/home/Latest.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import usePhotos from '../../hooks/usePhotos';
3 | import HomePhotoList from '../others/photo-list/HomePhotoList';
4 |
5 | const Latest = () => {
6 | const [allPhotos, setAllPhotos] = useState();
7 | const [loading, setLoading] = useState(false);
8 |
9 | const userId = localStorage.getItem('userId');
10 |
11 | const variables = {
12 | checkUserLike: userId,
13 | checkUserCollect: userId,
14 | first: 20,
15 | };
16 |
17 | const { photos, fetchMore, hasNextPage } = usePhotos(variables);
18 |
19 | useEffect(async () => {
20 | if (photos) {
21 | const temp = photos && photos.edges
22 | ? photos.edges.map((edge) => edge.node)
23 | : [];
24 |
25 | setAllPhotos(temp);
26 | setLoading(false);
27 | }
28 | }, [photos]);
29 |
30 | const clickFetchMore = () => {
31 | fetchMore();
32 | setLoading(true);
33 | };
34 |
35 | return (
36 |
37 |
44 |
45 | );
46 | };
47 |
48 | export default Latest;
49 |
--------------------------------------------------------------------------------
/src/components/home/TypeList.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import usePhotos from '../../hooks/usePhotos';
3 | import HomePhotoList from '../others/photo-list/HomePhotoList';
4 |
5 | const TypeList = ({ type }) => {
6 | const [allPhotos, setAllPhotos] = useState();
7 | const [loading, setLoading] = useState(false);
8 |
9 | const userId = localStorage.getItem('userId');
10 |
11 | const variables = {
12 | checkUserLike: userId,
13 | checkUserCollect: userId,
14 | first: 20,
15 | searchKeyword: type,
16 | };
17 |
18 | const { photos, fetchMore, hasNextPage } = usePhotos(variables);
19 |
20 | useEffect(async () => {
21 | if (photos) {
22 | const temp = photos && photos.edges
23 | ? photos.edges.map((edge) => edge.node)
24 | : [];
25 |
26 | setAllPhotos(temp);
27 | setLoading(false);
28 | }
29 | }, [photos]);
30 |
31 | const clickFetchMore = () => {
32 | fetchMore();
33 | setLoading(true);
34 | };
35 |
36 | return (
37 |
38 |
45 |
46 | );
47 | };
48 |
49 | export default TypeList;
50 |
--------------------------------------------------------------------------------
/src/components/others/CategoryBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CategoryCard from './photo-card/CategoryCard';
3 |
4 | const CategoryBar = () => {
5 | const category = [{
6 | id: 'YddPZXbgHkBRf4MVkDpXM',
7 | title: 'Photograph',
8 | cover: 'https://cdn.philoart.io/700x700/8/FS4MIHl-LbgSc25.jpg',
9 | },
10 | {
11 | id: 'eEWmzhJf3hR7E0NiJU98l',
12 | title: 'Paintings',
13 | cover: 'https://cdn.philoart.io/c/700x700/zu3VmDeCCM55iPp2zZAVZ.jpg',
14 | },
15 | {
16 | id: 'yibOLLFlIQtchyC6b5osL',
17 | title: 'Digital Art',
18 | cover: 'https://cdn.philoart.io/e/700x700/psYTmeZf1z6O2jukzPlyl.jpg',
19 | }];
20 |
21 | return (
22 |
23 |
24 |
25 | {category.map((collection) => (
26 |
27 |
28 |
29 | ))}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default CategoryBar;
37 |
--------------------------------------------------------------------------------
/src/components/others/LicenseButton.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
3 |
4 | const options = ['CC BY', 'CC BY-SA', 'CC BY-NC', 'CC BY-NC-SA', 'CC-BY-ND', 'CC BY-NC-ND'];
5 |
6 | // eslint-disable-next-line no-unused-vars
7 | const LicenseButton = ({ setLicense }) => {
8 | const [selected, setSelected] = useState('CC BY-NC');
9 |
10 | useEffect(() => {
11 | // setLicense(options[selectedIndex]);
12 | // setLicense(1);
13 | }, [selected]);
14 |
15 | const handleToggle = (option) => {
16 | setSelected(option);
17 | setLicense(option);
18 | };
19 |
20 | return (
21 |
22 |
23 |
24 | {selected}
25 |
26 |
27 |
28 | {options.map((option) => (
29 | handleToggle(option)}>
30 | {option}
31 |
32 | ))}
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default LicenseButton;
40 |
--------------------------------------------------------------------------------
/src/components/others/NavSearchBar.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | import React from 'react';
3 | import { useHistory } from 'react-router-dom';
4 | import { Form } from 'react-bootstrap';
5 | import useField from '../../hooks/useField';
6 |
7 | const NavSearchBar = ({ placeholder }) => {
8 | const searchKeyword = useField('text');
9 | const history = useHistory();
10 | const handleSearch = async (event) => {
11 | event.preventDefault();
12 | try {
13 | const query = searchKeyword.value.split(' ');
14 |
15 | if (query) {
16 | history.push(`/search?q=${query}`);
17 | } else history.push('/discover');
18 | } catch (e) {
19 | // eslint-disable-next-line no-console
20 | console.log(e);
21 | }
22 | };
23 |
24 | return (
25 |
26 |
29 |
30 | );
31 | };
32 |
33 | export default NavSearchBar;
34 |
--------------------------------------------------------------------------------
/src/components/others/PhotoRelatedTagBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TagBar from './TagBar';
3 |
4 | const PhotoRelatedTagBar = ({ photo }) => {
5 | if (photo === undefined) return null;
6 |
7 | const tags1 = photo.tags;
8 |
9 | let tags = tags1.split(',');
10 |
11 | if (tags.length > 10) {
12 | tags = tags.slice(0, 10);
13 | }
14 |
15 | return (
16 |
17 |
Related tags
18 |
19 |
20 | );
21 | };
22 |
23 | export default PhotoRelatedTagBar;
24 |
--------------------------------------------------------------------------------
/src/components/others/RelatedTagBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TagBar from './TagBar';
3 |
4 | const RelatedTagBar = ({ allPhotos }) => {
5 | if (allPhotos === undefined) return null;
6 |
7 | if (allPhotos.length === 0) {
8 | return (
9 |
10 |
No result
11 |
12 | );
13 | }
14 |
15 | const photo = allPhotos[1] || allPhotos[0];
16 | const tags1 = photo.tags;
17 |
18 | let tags = tags1.split(',');
19 |
20 | if (tags.length > 10) {
21 | tags = tags.slice(0, 10);
22 | }
23 |
24 | return (
25 |
26 |
Related tags
27 |
28 |
29 | );
30 | };
31 |
32 | export default RelatedTagBar;
33 |
--------------------------------------------------------------------------------
/src/components/others/StatusButton.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
3 |
4 | const options = ['None', 'Selling', 'Sold', 'Unavailable'];
5 |
6 | // eslint-disable-next-line no-unused-vars
7 | const StatusButton = ({ setStatus }) => {
8 | const [selected, setSelected] = useState('None');
9 |
10 | useEffect(() => {
11 | // setLicense(options[selectedIndex]);
12 | // setLicense(1);
13 | }, [selected]);
14 |
15 | const handleToggle = (option) => {
16 | setSelected(option);
17 | setStatus(option);
18 | };
19 |
20 | return (
21 |
22 |
23 |
24 | {selected}
25 |
26 |
27 |
28 | {options.map((option) => (
29 | handleToggle(option)}>
30 | {option}
31 |
32 | ))}
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default StatusButton;
40 |
--------------------------------------------------------------------------------
/src/components/others/TagBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const TagBar = ({ tagToShow }) => {
5 | let tags = ['nature', 'animals', 'people', 'travel', 'food', 'sea', 'texture', 'interiors', 'art'];
6 | if (tagToShow) tags = tagToShow;
7 |
8 | return (
9 |
10 |
11 |
12 | {tags.map((tag) => (
13 |
14 | {tag}
15 |
16 | ))}
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default TagBar;
24 |
--------------------------------------------------------------------------------
/src/components/others/TextInput.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | import React from 'react';
3 | import { useField } from 'formik';
4 |
5 | const TextInput = ({ label, info, ...props }) => {
6 | // useField() returns [formik.getFieldProps(), formik.getFieldMeta()]
7 | // which we can spread on . We can use field meta to show an error
8 | // message if the field is invalid and it has been touched (i.e. visited)
9 | const [field, meta] = useField(props);
10 | return (
11 |
12 |
13 |
14 | {label}
15 | {info}
16 |
17 |
18 |
19 | {meta.error ? (
20 |
{meta.error}
21 | ) : null}
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default TextInput;
32 |
--------------------------------------------------------------------------------
/src/components/others/TextInputDescription.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | import React from 'react';
3 | import { useField } from 'formik';
4 |
5 | const TextInputDescription = ({ label, info, ...props }) => {
6 | // useField() returns [formik.getFieldProps(), formik.getFieldMeta()]
7 | // which we can spread on . We can use field meta to show an error
8 | // message if the field is invalid and it has been touched (i.e. visited)
9 | const [field, meta] = useField(props);
10 | return (
11 |
12 |
13 |
14 | {label}
15 | {info}
16 |
17 |
18 |
19 | {meta.error ? (
20 |
{meta.error}
21 | ) : null}
22 |
23 |
24 |
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default TextInputDescription;
38 |
--------------------------------------------------------------------------------
/src/components/others/TokenExpireModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useApolloClient } from '@apollo/client';
3 | import { useHistory } from 'react-router-dom';
4 | import { Modal } from 'react-bootstrap';
5 |
6 | const TokenExpireModal = ({
7 | showTokenExpireModal,
8 | setShowTokenExpireModal,
9 | }) => {
10 | const history = useHistory();
11 | const client = useApolloClient();
12 |
13 | const redirectToSignIn = async () => {
14 | setShowTokenExpireModal(false);
15 | localStorage.clear();
16 | client.resetStore();
17 | history.push('/signin');
18 | };
19 |
20 | const handleLogout = async () => {
21 | localStorage.clear();
22 | client.resetStore();
23 | setShowTokenExpireModal(false);
24 | };
25 |
26 | return (
27 |
28 | setShowTokenExpireModal(false)}
31 | centered
32 | dialogClassName="modal-90w"
33 | aria-labelledby="example-custom-modal-styling-title"
34 | >
35 |
36 |
37 | Session token has expired, please sign in again.
38 |
39 |
40 |
41 | handleLogout()}>
42 | Cancel
43 |
44 | redirectToSignIn()}>
45 | Sign In
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default TokenExpireModal;
54 |
--------------------------------------------------------------------------------
/src/components/others/TypeButton.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
3 |
4 | const options = ['Photograph', 'Painting', 'Digital Art', 'Drawing'];
5 |
6 | // eslint-disable-next-line no-unused-vars
7 | const TypeButton = ({ setType }) => {
8 | const [selected, setSelected] = useState('Photograph');
9 |
10 | useEffect(() => {
11 | // setLicense(options[selectedIndex]);
12 | // setLicense(1);
13 | }, [selected]);
14 |
15 | const handleToggle = (option) => {
16 | setSelected(option);
17 | setType(option);
18 | };
19 |
20 | return (
21 |
22 |
23 |
24 | {selected}
25 |
26 |
27 |
28 | {options.map((option) => (
29 | handleToggle(option)}>
30 | {option}
31 |
32 | ))}
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default TypeButton;
40 |
--------------------------------------------------------------------------------
/src/components/others/UserCollectionsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { css } from '@emotion/react';
3 | import BeatLoader from 'react-spinners/BeatLoader';
4 | import { Card } from 'react-bootstrap';
5 | import Masonry from 'react-masonry-css';
6 | import galleryIcon from '../../img/galleryIcon.jpg';
7 | import '../../mdb.css';
8 |
9 | const override = css`
10 | display: flex;
11 | justify-content: center;
12 | align-item: center;
13 | margin: 3rem;
14 | margin-bottom: 6rem;
15 | `;
16 |
17 | const breakpointColumnsObj = {
18 | default: 3,
19 | 800: 2,
20 | 500: 1,
21 | };
22 | const INIT_COVER = galleryIcon;
23 |
24 | const UserCollectionsList = ({ loadingList, allCollections, collectSinglePhoto }) => {
25 | if (loadingList) {
26 | return (
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | if (!allCollections || allCollections.length === 0) {
34 | return (
35 |
36 |
No collection
37 |
38 | );
39 | }
40 |
41 | return (
42 |
43 |
48 | {allCollections.map((collection) => (
49 |
50 | { collectSinglePhoto(collection); }}
53 | onKeyPress={() => collectSinglePhoto(collection)}
54 | role="button"
55 | tabIndex="0"
56 | >
57 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | {collection.title}
69 |
70 |
71 |
72 |
73 |
74 |
75 | ))}
76 |
77 |
78 | );
79 | };
80 |
81 | export default UserCollectionsList;
82 |
--------------------------------------------------------------------------------
/src/components/others/button/LoadMore.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { css } from '@emotion/react';
3 | import BeatLoader from 'react-spinners/BeatLoader';
4 |
5 | const override = css`
6 | display: flex;
7 | justify-content: center;
8 | align-item: center;
9 | margin-top: 3rem;
10 | `;
11 |
12 | const LoadMore = ({
13 | hasNextPage, clickFetchMore, loading, title,
14 | }) => (
15 |
16 | {hasNextPage && title && (
17 |
18 | { loading && (
) }
19 |
20 |
21 |
22 | {title}
23 |
24 |
25 |
26 | )}
27 | {hasNextPage && !title && (
28 |
29 | { loading && (
) }
30 |
31 |
32 |
33 | More photos
34 |
35 |
36 |
37 | )}
38 | {!hasNextPage && (
39 |
40 |
The end
41 |
42 | )}
43 |
44 | );
45 |
46 | export default LoadMore;
47 |
--------------------------------------------------------------------------------
/src/components/others/button/edit-collection-btn/CollectionDropdownButton.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | import React from 'react';
3 | import Grid from '@material-ui/core/Grid';
4 | import Button from '@material-ui/core/Button';
5 | import ButtonGroup from '@material-ui/core/ButtonGroup';
6 | import ClickAwayListener from '@material-ui/core/ClickAwayListener';
7 | import Grow from '@material-ui/core/Grow';
8 | import Paper from '@material-ui/core/Paper';
9 | import Popper from '@material-ui/core/Popper';
10 | import MenuItem from '@material-ui/core/MenuItem';
11 | import MenuList from '@material-ui/core/MenuList';
12 |
13 | const options = ['Edit', 'Delete'];
14 |
15 | export default function CollectionDropdownButton({
16 | setShowEditCollectionModal,
17 | setShowDeleteModal,
18 | }) {
19 | const [open, setOpen] = React.useState(false);
20 | const anchorRef = React.useRef(null);
21 |
22 | const handleMenuItemClick = (event, index) => {
23 | if (options[index] === 'Edit') {
24 | setShowEditCollectionModal(true);
25 | } else if (options[index] === 'Delete') {
26 | setShowDeleteModal(true);
27 | }
28 | setOpen(false);
29 | };
30 |
31 | const handleToggle = () => {
32 | setOpen((prevOpen) => !prevOpen);
33 | };
34 |
35 | const handleClose = (event) => {
36 | if (anchorRef.current && anchorRef.current.contains(event.target)) {
37 | return;
38 | }
39 |
40 | setOpen(false);
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 |
56 |
57 |
58 |
59 |
60 | {({ TransitionProps, placement }) => (
61 |
67 |
68 |
69 |
81 |
82 |
83 |
84 | )}
85 |
86 |
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/others/button/edit-collection-btn/CollectionDropdownButtonDotIcon.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | import React from 'react';
3 | import Grid from '@material-ui/core/Grid';
4 | import Button from '@material-ui/core/Button';
5 | import ButtonGroup from '@material-ui/core/ButtonGroup';
6 | import ClickAwayListener from '@material-ui/core/ClickAwayListener';
7 | import Grow from '@material-ui/core/Grow';
8 | import Paper from '@material-ui/core/Paper';
9 | import Popper from '@material-ui/core/Popper';
10 | import MenuItem from '@material-ui/core/MenuItem';
11 | import MenuList from '@material-ui/core/MenuList';
12 |
13 | const options = ['Edit', 'Delete'];
14 |
15 | export default function CollectionDropdownButton({
16 | setShowEditCollectionModal,
17 | setShowDeleteModal,
18 | }) {
19 | const [open, setOpen] = React.useState(false);
20 | const anchorRef = React.useRef(null);
21 |
22 | const handleMenuItemClick = (event, index) => {
23 | if (options[index] === 'Edit') {
24 | setShowEditCollectionModal(true);
25 | } else if (options[index] === 'Delete') {
26 | setShowDeleteModal(true);
27 | }
28 | setOpen(false);
29 | };
30 |
31 | const handleToggle = () => {
32 | setOpen((prevOpen) => !prevOpen);
33 | };
34 |
35 | const handleClose = (event) => {
36 | if (anchorRef.current && anchorRef.current.contains(event.target)) {
37 | return;
38 | }
39 |
40 | setOpen(false);
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 |
56 |
57 |
58 |
59 |
60 | {({ TransitionProps, placement }) => (
61 |
67 |
68 |
69 |
81 |
82 |
83 |
84 | )}
85 |
86 |
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/others/button/edit-photo-btn/DropdownButton.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | import React from 'react';
3 | import Grid from '@material-ui/core/Grid';
4 | import Button from '@material-ui/core/Button';
5 | import ButtonGroup from '@material-ui/core/ButtonGroup';
6 | import ClickAwayListener from '@material-ui/core/ClickAwayListener';
7 | import Grow from '@material-ui/core/Grow';
8 | import Paper from '@material-ui/core/Paper';
9 | import Popper from '@material-ui/core/Popper';
10 | import MenuItem from '@material-ui/core/MenuItem';
11 | import MenuList from '@material-ui/core/MenuList';
12 |
13 | const options = ['Edit', 'Delete'];
14 |
15 | export default function DropdownButton({
16 | setShowEditModal,
17 | setShowDeleteModal,
18 | }) {
19 | const [open, setOpen] = React.useState(false);
20 | const anchorRef = React.useRef(null);
21 |
22 | const handleMenuItemClick = (event, index) => {
23 | if (options[index] === 'Edit') {
24 | setShowEditModal(true);
25 | } else if (options[index] === 'Delete') {
26 | setShowDeleteModal(true);
27 | }
28 | setOpen(false);
29 | };
30 |
31 | const handleToggle = () => {
32 | setOpen((prevOpen) => !prevOpen);
33 | };
34 |
35 | const handleClose = (event) => {
36 | if (anchorRef.current && anchorRef.current.contains(event.target)) {
37 | return;
38 | }
39 |
40 | setOpen(false);
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 |
56 |
57 |
58 |
59 |
60 | {({ TransitionProps, placement }) => (
61 |
67 |
68 |
69 |
81 |
82 |
83 |
84 | )}
85 |
86 |
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/others/photo-card/CategoryCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card } from 'react-bootstrap';
3 | import { useHistory } from 'react-router-dom';
4 | import galleryIcon from '../../../img/galleryIcon.jpg';
5 | import '../../../mdb.css';
6 |
7 | const INIT_COVER = galleryIcon;
8 |
9 | const CategoryCard = ({ collection }) => {
10 | if (!collection) return null;
11 | const history = useHistory();
12 |
13 | const openCollection = (collectionId) => {
14 | history.push(`/collection/${collectionId}`);
15 | };
16 |
17 | return (
18 |
19 |
20 | { openCollection(collection.id); }}
23 | onKeyPress={() => openCollection(collection.id)}
24 | role="button"
25 | tabIndex="0"
26 | >
27 |
32 |
33 |
{collection.title}
34 |
35 |
36 |
37 |
38 |
39 | {collection.title}
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default CategoryCard;
49 |
--------------------------------------------------------------------------------
/src/components/others/photo-list/HomePhotoList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { css } from '@emotion/react';
3 | import PacmanLoader from 'react-spinners/PacmanLoader';
4 | import HomePhotoListContainer from './HomePhotoListContainer';
5 |
6 | const override = css`
7 | display: flex;
8 | justify-content: center;
9 | align-item: center;
10 | margin: 3rem;
11 | margin-bottom: 6rem;
12 | `;
13 |
14 | const HomePhotoList = ({
15 | allPhotos, setAllPhotos, clickFetchMore, loading, column, hasNextPage,
16 | }) => {
17 | if (allPhotos === undefined) {
18 | return (
19 |
22 | );
23 | }
24 |
25 | return (
26 |
27 |
35 |
36 | );
37 | };
38 |
39 | export default HomePhotoList;
40 |
--------------------------------------------------------------------------------
/src/components/others/photo-list/HomePhotoListContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useHistory } from 'react-router-dom';
3 | import Masonry from 'react-masonry-css';
4 | import { nanoid } from 'nanoid';
5 | import LoadMore from '../button/LoadMore';
6 | import useLikePhoto from '../../../hooks/useLikePhoto';
7 | import PhotoCard from '../photo-card/PhotoCard';
8 | import useUnlikePhoto from '../../../hooks/useUnlikePhoto';
9 |
10 | const breakpointColumnsObj = {
11 | default: 3,
12 | 800: 2,
13 | 500: 1,
14 | };
15 |
16 | const breakpointColumnsObj2 = {
17 | default: 3,
18 | 800: 2,
19 | 500: 1,
20 | };
21 |
22 | const HomePhotoListContainer = ({
23 | allPhotos, setAllPhotos, clickFetchMore, loading, column, hasNextPage,
24 | }) => {
25 | const [likePhoto] = useLikePhoto();
26 | const [unlikePhoto] = useUnlikePhoto();
27 | const history = useHistory();
28 | const token = localStorage.getItem('token');
29 |
30 | const likeSinglePhoto = async (photo) => {
31 | if (!token) {
32 | history.push('/signin');
33 | } else {
34 | const temp = allPhotos
35 | .map((obj) => (obj.id === photo.id ? { ...obj, isLiked: !obj.isLiked } : obj));
36 | setAllPhotos(temp);
37 | if (photo.isLiked) {
38 | await unlikePhoto({ photoId: photo.id });
39 | } else {
40 | await likePhoto({ photoId: photo.id });
41 | }
42 | }
43 | };
44 |
45 | return (
46 |
47 |
48 |
53 | {allPhotos.map((photo) => (
54 |
60 | ))}
61 |
62 |
63 |
68 |
69 | );
70 | };
71 |
72 | export default HomePhotoListContainer;
73 |
--------------------------------------------------------------------------------
/src/components/others/search-bar/DropdownButton.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
3 |
4 | const options = ['All', 'Picky'];
5 |
6 | export default function DropdownButton({ setSearchRange }) {
7 | const [selectedIndex, setSelectedIndex] = useState(0);
8 |
9 | useEffect(() => {
10 | setSearchRange(options[selectedIndex]);
11 | }, [selectedIndex]);
12 |
13 | const handleToggle = () => {
14 | setSelectedIndex(selectedIndex === 0 ? 1 : 0);
15 | };
16 |
17 | return (
18 |
19 |
20 |
21 | {options[selectedIndex]}
22 |
23 |
24 |
25 |
26 | {options[selectedIndex === 0 ? 1 : 0]}
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/others/search-bar/SearchBar.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | /* eslint-disable react/jsx-props-no-spreading */
3 | import React, { useState } from 'react';
4 | import { useHistory } from 'react-router-dom';
5 | import { Form } from 'react-bootstrap';
6 | // import { makeStyles } from '@material-ui/core/styles';
7 | // import DropdownButton from './DropdownButton';
8 | import useField from '../../../hooks/useField';
9 |
10 | // const useStyles = makeStyles((theme) => ({
11 | // margin: {
12 | // margin: theme.spacing(1),
13 | // },
14 | // extendedIcon: {
15 | // marginRight: theme.spacing(1),
16 | // },
17 | // }));
18 |
19 | const SearchBar = () => {
20 | const searchKeyword = useField('text');
21 | // const [searchRange, setSearchRange] = useState('All');
22 | const history = useHistory();
23 | // const classes = useStyles();
24 |
25 | const handleSearch = async (event) => {
26 | event.preventDefault();
27 | try {
28 | if (!searchKeyword.value) {
29 | history.push('/discover');
30 | return;
31 | }
32 | history.push(`/search?q=${searchKeyword.value}`);
33 | } catch (e) {
34 | // eslint-disable-next-line no-console
35 | console.log(e);
36 | }
37 | };
38 |
39 | return (
40 |
50 | );
51 | };
52 |
53 | export default SearchBar;
54 |
--------------------------------------------------------------------------------
/src/components/photo-page/Colorbox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Colorbox = ({ color }) => (
4 |
5 |
6 |
7 |
8 |
9 | {color}
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | export default Colorbox;
18 |
--------------------------------------------------------------------------------
/src/components/photo-page/DeletePhotoModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useHistory } from 'react-router-dom';
3 | import { Modal, Alert } from 'react-bootstrap';
4 | import useDeletePhoto from '../../hooks/useDeletePhoto';
5 |
6 | const DeletePhotoModal = ({
7 | photo,
8 | showDeleteModal,
9 | setShowDeleteModal,
10 | }) => {
11 | const [errorInfo, setErrorInfo] = useState('');
12 | const [successInfo, setSuccessInfo] = useState('');
13 | const [deletePhoto] = useDeletePhoto();
14 | const history = useHistory();
15 |
16 | const deleteSinglePhoto = async () => {
17 | try {
18 | await deletePhoto({ id: photo.id });
19 | setSuccessInfo('Photo is deleted');
20 | setTimeout(() => { setSuccessInfo(''); setShowDeleteModal(false); history.goBack(); }, 3000);
21 | } catch (e) {
22 | setErrorInfo(e.message);
23 | setTimeout(() => { setErrorInfo(''); }, 3000);
24 | }
25 | };
26 |
27 | return (
28 |
29 |
setShowDeleteModal(false)}
32 | centered
33 | dialogClassName="modal-90w"
34 | aria-labelledby="example-custom-modal-styling-title"
35 | >
36 |
37 |
38 | Delete this photo?
39 |
40 |
41 | {errorInfo && (
42 |
43 | {errorInfo}
44 |
45 | )}
46 | {successInfo && (
47 |
48 | {successInfo}
49 |
50 | )}
51 |
52 | setShowDeleteModal(false)}>
53 | Close
54 |
55 | deleteSinglePhoto()}>
56 |
57 | Delete
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default DeletePhotoModal;
66 |
--------------------------------------------------------------------------------
/src/components/photo-page/PhotoDetails.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import usePhoto from '../../hooks/usePhoto';
4 | import PhotoRelatedTagBar from '../others/PhotoRelatedTagBar';
5 | import PhotoDetailsContainer from './PhotoDetailsContainer';
6 | import RelatedPhotos from './RelatedPhotos';
7 |
8 | const PhotoDetails = () => {
9 | const [photoToShow, setPhotoToShow] = useState();
10 | const { id } = useParams();
11 | const userId = localStorage.getItem('userId');
12 |
13 | const { photo } = usePhoto({
14 | id,
15 | checkUserLike: userId,
16 | });
17 |
18 | useEffect(() => {
19 | if (photo) {
20 | setPhotoToShow(photo);
21 | }
22 | }, [photo]);
23 |
24 | return (
25 |
33 | );
34 | };
35 |
36 | export default PhotoDetails;
37 |
--------------------------------------------------------------------------------
/src/components/photo-page/PhotoInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const PhotoInfo = ({ photo }) => (
4 |
5 |
6 |
7 |
8 |
9 | Likes
10 |
11 |
12 | {photo.likeCount}
13 |
14 |
15 |
16 |
17 |
18 |
19 | Collections
20 |
21 |
22 | {photo.collectionCount}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Downloads
31 |
32 |
33 | {photo.downloadCount}
34 |
35 |
36 |
37 |
38 | );
39 |
40 | export default PhotoInfo;
41 |
--------------------------------------------------------------------------------
/src/components/photo-page/RelatedPhotos.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import usePhotos from '../../hooks/usePhotos';
3 | import HomePhotoList from '../others/photo-list/HomePhotoList';
4 |
5 | const RelatedPhotos = ({
6 | photoToShow,
7 | }) => {
8 | const [allPhotos, setAllPhotos] = useState();
9 | const [loading, setLoading] = useState(false);
10 | const userId = localStorage.getItem('userId');
11 |
12 | const tags1 = photoToShow && photoToShow.tags;
13 |
14 | const tags = tags1 && tags1.split(',');
15 |
16 | const variables = {
17 | searchKeyword: tags && tags[0],
18 | checkUserLike: userId,
19 | first: 10,
20 | };
21 |
22 | const { photos, fetchMore } = usePhotos(variables);
23 |
24 | useEffect(() => {
25 | if (photos) {
26 | const temp = photos && photos.edges
27 | ? photos.edges.map((edge) => edge.node)
28 | : [];
29 |
30 | setAllPhotos(temp);
31 | setLoading(false);
32 | }
33 | }, [photos]);
34 |
35 | const clickFetchMore = () => {
36 | fetchMore();
37 | setLoading(true);
38 | };
39 |
40 | return (
41 |
42 |
43 |
44 |
Similar photos
45 |
46 |
47 |
53 |
54 | );
55 | };
56 |
57 | export default RelatedPhotos;
58 |
--------------------------------------------------------------------------------
/src/components/profile/Settings.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import {
3 | Nav, Image, Tab, Row, Col,
4 | } from 'react-bootstrap';
5 | import { css } from '@emotion/react';
6 | import PacmanLoader from 'react-spinners/PacmanLoader';
7 | import EditProfile from './edit-profile/EditProfile';
8 | import ChangePassword from './change-password/ChangePassword';
9 | import DeleteAccount from './delete-account/DeleteAccount';
10 | import UpdateAvatar from './update-avatar/UpdateAvatar';
11 | import useAuthorizedUser from '../../hooks/useAuthorizedUser';
12 |
13 | const override = css`
14 | display: flex;
15 | justify-content: center;
16 | align-item: center;
17 | margin: 3rem;
18 | margin-bottom: 6rem;
19 | `;
20 |
21 | const Settings = () => {
22 | const { authorizedUser } = useAuthorizedUser();
23 | const [user, setUser] = useState();
24 | const userId = localStorage.getItem('userId');
25 | const [preview, setPreview] = useState('');
26 |
27 | useEffect(() => {
28 | if (authorizedUser) {
29 | setUser(authorizedUser);
30 | }
31 | }, [authorizedUser]);
32 |
33 | console.log(authorizedUser);
34 |
35 | if (!authorizedUser) {
36 | return (
37 |
40 | );
41 | }
42 |
43 | const profileImage = user && user.profileImage
44 | ? user.profileImage
45 | : 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png';
46 |
47 | return (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
{user && user.username}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | Edit Profile
63 |
64 |
65 | Update Avatar
66 |
67 |
68 | Change Password
69 |
70 |
71 | Delete Account
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | );
95 | };
96 |
97 | export default Settings;
98 |
--------------------------------------------------------------------------------
/src/components/profile/UserArtworks.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import useUserLikes from '../../hooks/useUserLikes';
4 | import HomePhotoList from '../others/photo-list/HomePhotoList';
5 |
6 | const UserArtworks = () => {
7 | const [allPhotos, setAllPhotos] = useState();
8 | const [loading, setLoading] = useState(false);
9 | const { username } = useParams();
10 | const userId = localStorage.getItem('userId');
11 |
12 | const variables = {
13 | username,
14 | checkUserLike: userId,
15 | first: 15,
16 | };
17 |
18 | const { likes, fetchMore, hasNextPage } = useUserLikes(variables);
19 |
20 | useEffect(() => {
21 | if (likes) {
22 | const temp = likes && likes.edges
23 | ? likes.edges.map((edge) => edge.node.photo)
24 | : [];
25 |
26 | setAllPhotos(temp);
27 | setLoading(false);
28 | }
29 | }, [likes]);
30 |
31 | const clickFetchMore = () => {
32 | fetchMore();
33 | setLoading(true);
34 | };
35 |
36 | return (
37 |
38 |
45 |
46 | );
47 | };
48 |
49 | export default UserArtworks;
50 |
--------------------------------------------------------------------------------
/src/components/profile/UserCollections.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { css } from '@emotion/react';
3 | import PacmanLoader from 'react-spinners/PacmanLoader';
4 | import { useParams } from 'react-router-dom';
5 | import Masonry from 'react-masonry-css';
6 | import '../../index.css';
7 | import useCollections from '../../hooks/useCollections';
8 | import CollectionCard from './user-collections/CollectionCard';
9 | import LoadMore from '../others/button/LoadMore';
10 |
11 | const override = css`
12 | display: flex;
13 | justify-content: center;
14 | align-item: center;
15 | margin: 3rem;
16 | margin-bottom: 6rem;
17 | `;
18 |
19 | const breakpointColumnsObj = {
20 | default: 3,
21 | 800: 2,
22 | 500: 1,
23 | };
24 |
25 | const UserCollections = () => {
26 | const [loading, setLoading] = useState(false);
27 | const [allCollections, setAllCollections] = useState();
28 | const authUsername = localStorage.getItem('username');
29 |
30 | const { username } = useParams();
31 | const { collections, fetchMore, hasNextPage } = useCollections({
32 | username,
33 | first: 30,
34 | });
35 |
36 | useEffect(() => {
37 | if (collections) {
38 | const temp = collections && collections.edges
39 | ? collections.edges.map((edge) => edge.node)
40 | : [];
41 |
42 | setAllCollections(temp);
43 | setLoading(false);
44 | }
45 | }, [collections]);
46 |
47 | const clickFetchMore = () => {
48 | fetchMore();
49 | setLoading(true);
50 | };
51 |
52 | if (allCollections === undefined) {
53 | return (
54 |
57 | );
58 | }
59 |
60 | return (
61 |
62 |
67 | {allCollections.map((collection) => (
68 |
75 | ))}
76 |
77 |
83 |
84 | );
85 | };
86 |
87 | export default UserCollections;
88 |
--------------------------------------------------------------------------------
/src/components/profile/UserLikes.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import useUserLikes from '../../hooks/useUserLikes';
4 | import HomePhotoList from '../others/photo-list/HomePhotoList';
5 |
6 | const UserLikes = () => {
7 | const [allPhotos, setAllPhotos] = useState();
8 | const [loading, setLoading] = useState(false);
9 | const { username } = useParams();
10 | const userId = localStorage.getItem('userId');
11 |
12 | const variables = {
13 | username,
14 | checkUserLike: userId,
15 | first: 15,
16 | };
17 |
18 | const { likes, fetchMore, hasNextPage } = useUserLikes(variables);
19 |
20 | useEffect(() => {
21 | if (likes) {
22 | const temp = likes && likes.edges
23 | ? likes.edges.map((edge) => edge.node.photo)
24 | : [];
25 |
26 | setAllPhotos(temp);
27 | setLoading(false);
28 | }
29 | }, [likes]);
30 |
31 | const clickFetchMore = () => {
32 | fetchMore();
33 | setLoading(true);
34 | };
35 |
36 | return (
37 |
38 |
45 |
46 | );
47 | };
48 |
49 | export default UserLikes;
50 |
--------------------------------------------------------------------------------
/src/components/profile/UserPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Switch, Route, useRouteMatch, useHistory,
4 | } from 'react-router-dom';
5 | import { Nav, Image } from 'react-bootstrap';
6 | import Profile from './Profile';
7 | import useAuthorizedUser from '../../hooks/useAuthorizedUser';
8 |
9 | const UserPage = () => {
10 | const { path, url } = useRouteMatch();
11 | const { authorizedUser } = useAuthorizedUser();
12 | const history = useHistory();
13 |
14 | if (!authorizedUser) return null;
15 |
16 | const profileImage = authorizedUser && authorizedUser.profileImage
17 | ? authorizedUser.profileImage
18 | : 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png';
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
{authorizedUser.username}
28 |
29 |
30 |
31 |
32 | Likes
33 |
34 |
35 | Collections
36 |
37 |
38 |
39 |
40 |
41 | Please select a topic.
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default UserPage;
52 |
--------------------------------------------------------------------------------
/src/components/profile/UserPhotos.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import usePhotos from '../../hooks/usePhotos';
4 | import HomePhotoList from '../others/photo-list/HomePhotoList';
5 |
6 | const UserPhotos = ({ type }) => {
7 | const [allPhotos, setAllPhotos] = useState();
8 | const [loading, setLoading] = useState(false);
9 | const { username } = useParams();
10 | const userId = localStorage.getItem('userId');
11 |
12 | const variables = {
13 | username,
14 | checkUserLike: userId,
15 | first: 20,
16 | searchKeyword: type,
17 | };
18 |
19 | const { photos, fetchMore, hasNextPage } = usePhotos(variables);
20 |
21 | useEffect(() => {
22 | if (photos) {
23 | const temp = photos && photos.edges
24 | ? photos.edges.map((edge) => edge.node)
25 | : [];
26 |
27 | setAllPhotos(temp);
28 | setLoading(false);
29 | }
30 | }, [photos]);
31 |
32 | const clickFetchMore = () => {
33 | fetchMore();
34 | setLoading(true);
35 | };
36 |
37 | return (
38 |
39 |
46 |
47 | );
48 | };
49 |
50 | export default UserPhotos;
51 |
--------------------------------------------------------------------------------
/src/components/profile/change-password/ChangePassword.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import useChangePassword from '../../../hooks/useChangePassword';
3 | import ChangePasswordContainer from './ChangePasswordContainer';
4 |
5 | const ChangePassword = () => {
6 | const [changePassword] = useChangePassword();
7 | const [errorInfo, setErrorInfo] = useState('');
8 | const [successInfo, setSuccessInfo] = useState('');
9 | const [loading, setLoading] = useState(false);
10 |
11 | const initialValues = {
12 | currentPassword: '',
13 | newPassword: '',
14 | };
15 |
16 | const onSubmit = async (values) => {
17 | const variables = {
18 | currentPassword: values.currentPassword,
19 | newPassword: values.newPassword,
20 | };
21 | setLoading(true);
22 |
23 | try {
24 | await changePassword(variables);
25 | setSuccessInfo('Password changed');
26 | setTimeout(() => { setSuccessInfo(''); }, 3000);
27 | } catch (e) {
28 | setErrorInfo(e.message);
29 | setTimeout(() => { setErrorInfo(''); }, 3000);
30 | }
31 | setLoading(false);
32 | };
33 |
34 | return (
35 |
42 | );
43 | };
44 |
45 | export default ChangePassword;
46 |
--------------------------------------------------------------------------------
/src/components/profile/change-password/ChangePasswordContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik } from 'formik';
3 | import { Alert } from 'react-bootstrap';
4 | import * as Yup from 'yup';
5 |
6 | import ChangePasswordForm from './ChangePasswordForm';
7 |
8 | const validationSchema = Yup.object({
9 | currentPassword: Yup.string()
10 | .min(6, 'Must be 6 characters or more'),
11 | newPassword: Yup.string()
12 | .min(6, 'Must be 6 characters or more'),
13 | });
14 |
15 | const ChangePasswordContainer = ({
16 | initialValues, onSubmit, errorInfo, successInfo, loading,
17 | }) => (
18 |
19 | {errorInfo && (
20 |
21 | {errorInfo}
22 |
23 | )}
24 | {successInfo && (
25 |
26 | {successInfo}
27 |
28 | )}
29 |
34 | {({ handleSubmit }) => }
35 |
36 |
37 | );
38 |
39 | export default ChangePasswordContainer;
40 |
--------------------------------------------------------------------------------
/src/components/profile/change-password/ChangePasswordForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button, Spinner } from 'react-bootstrap';
4 | import { Form } from 'formik';
5 |
6 | import TextInput from '../../others/TextInput';
7 |
8 | const ChangePasswordForm = ({ loading }) => (
9 |
52 | );
53 |
54 | export default ChangePasswordForm;
55 |
--------------------------------------------------------------------------------
/src/components/profile/delete-account/DeleteAccount.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button } from 'react-bootstrap';
3 | import DeleteAccountModal from './DeleteAccountModal';
4 |
5 | const DeleteAccount = () => {
6 | const [showDeleteModal, setShowDeleteModal] = useState(false);
7 | const token = localStorage.getItem('philoart-token');
8 |
9 | if (!token) return null;
10 |
11 | return (
12 |
13 |
14 |
15 |
Delete your account
16 |
17 |
18 |
19 | setShowDeleteModal(true)}
25 | >
26 |
27 | Delete
28 |
29 |
30 |
31 |
35 |
36 | );
37 | };
38 |
39 | export default DeleteAccount;
40 |
--------------------------------------------------------------------------------
/src/components/profile/delete-account/DeleteAccountModal.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import React, { useState } from 'react';
3 | import { useHistory } from 'react-router-dom';
4 | import { Modal, Alert } from 'react-bootstrap';
5 | import useDeleteUser from '../../../hooks/useDeleteUser';
6 |
7 | const DeleteAccountModal = ({
8 | showDeleteModal,
9 | setShowDeleteModal,
10 | }) => {
11 | const [errorInfo, setErrorInfo] = useState('');
12 | const [successInfo, setSuccessInfo] = useState('');
13 | const [deleteUser] = useDeleteUser();
14 | const history = useHistory();
15 | const userId = localStorage.getItem('philoart-userId');
16 |
17 | const deleteAccount = async () => {
18 | try {
19 | await deleteUser({ id: userId });
20 | setSuccessInfo('Account is deleted');
21 | setTimeout(() => { setSuccessInfo(''); setShowDeleteModal(false); history.push('/'); }, 3000);
22 | } catch (e) {
23 | setErrorInfo(e.message);
24 | setTimeout(() => { setErrorInfo(''); }, 3000);
25 | }
26 | };
27 |
28 | return (
29 |
30 |
setShowDeleteModal(false)}
33 | centered
34 | dialogClassName="modal-90w"
35 | aria-labelledby="example-custom-modal-styling-title"
36 | >
37 |
38 |
39 | Delete your account?
40 |
41 |
42 | {errorInfo && (
43 |
44 | {errorInfo}
45 |
46 | )}
47 | {successInfo && (
48 |
49 | {successInfo}
50 |
51 | )}
52 |
53 | setShowDeleteModal(false)}>
54 | Close
55 |
56 | deleteAccount()}>
57 |
58 | Delete
59 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default DeleteAccountModal;
67 |
--------------------------------------------------------------------------------
/src/components/profile/edit-profile/EditProfile.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import useUpdateProfile from '../../../hooks/useUpdateProfile';
3 | import EditProfileContainer from './EditProfileContainer';
4 |
5 | const EditProfile = ({ user }) => {
6 | const [updateProfile] = useUpdateProfile();
7 | const [errorInfo, setErrorInfo] = useState('');
8 | const [successInfo, setSuccessInfo] = useState('');
9 | const [loading, setLoading] = useState(false);
10 |
11 | if (!user) return null;
12 |
13 | const initialValues = {
14 | firstName: user.firstName,
15 | lastName: user.lastName || '',
16 | email: user.email,
17 | username: user.username,
18 | description: user.description,
19 | password: '',
20 | };
21 |
22 | const onSubmit = async (values) => {
23 | const variables = {
24 | firstName: values.firstName,
25 | lastName: values.lastName,
26 | email: values.email,
27 | username: values.username,
28 | description: values.description,
29 | password: values.password,
30 | };
31 | setLoading(true);
32 |
33 | try {
34 | await updateProfile(variables);
35 | setSuccessInfo('Profile updated');
36 | setTimeout(() => { setSuccessInfo(''); }, 3000);
37 | } catch (e) {
38 | setErrorInfo(e.message);
39 | setTimeout(() => { setErrorInfo(''); }, 3000);
40 | }
41 | setLoading(false);
42 | };
43 |
44 | return (
45 |
52 | );
53 | };
54 |
55 | export default EditProfile;
56 |
--------------------------------------------------------------------------------
/src/components/profile/edit-profile/EditProfileContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik } from 'formik';
3 | import { Alert } from 'react-bootstrap';
4 | import * as Yup from 'yup';
5 |
6 | import EditProfileForm from './EditProfileForm';
7 |
8 | const validationSchema = Yup.object({
9 | firstName: Yup.string()
10 | .max(15, 'Must be 15 characters or less')
11 | .required('Required'),
12 | lastName: Yup.string()
13 | .max(20, 'Must be 20 characters or less'),
14 | email: Yup.string()
15 | .email('Invalid email address')
16 | .required('Required'),
17 | username: Yup.string()
18 | .max(20, 'Must be 20 characters or less')
19 | .matches('^[a-zA-Z0-9_]*$', 'Invalid username')
20 | .required('Required'),
21 | description: Yup.string()
22 | .trim(),
23 | password: Yup.string()
24 | .min(6, 'Must be 6 characters or more'),
25 | });
26 |
27 | const EditProfileContainer = ({
28 | initialValues, onSubmit, errorInfo, successInfo, loading,
29 | }) => (
30 |
31 | {errorInfo && (
32 |
33 | {errorInfo}
34 |
35 | )}
36 | {successInfo && (
37 |
38 | {successInfo}
39 |
40 | )}
41 |
46 | {({ handleSubmit }) => }
47 |
48 |
49 | );
50 |
51 | export default EditProfileContainer;
52 |
--------------------------------------------------------------------------------
/src/components/profile/edit-profile/EditProfileForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button, Spinner } from 'react-bootstrap';
4 | import { Form } from 'formik';
5 |
6 | import TextInputDescription from '../../others/TextInputDescription';
7 | import TextInput from '../../others/TextInput';
8 |
9 | const EditProfileForm = ({ loading }) => (
10 |
91 | );
92 |
93 | export default EditProfileForm;
94 |
--------------------------------------------------------------------------------
/src/components/profile/update-avatar/AvatarEdit.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Avatar from 'react-avatar-edit';
3 | import {
4 | Alert, Button, Spinner,
5 | } from 'react-bootstrap';
6 |
7 | const AvatarEdit = ({
8 | onSubmit, errorInfo, successInfo, loading, setPreview,
9 | }) => {
10 | const onClose = () => {
11 | setPreview(null);
12 | };
13 |
14 | const onCrop = (previewNow) => {
15 | setPreview(previewNow);
16 | };
17 |
18 | return (
19 |
20 | {errorInfo && (
21 |
22 | {errorInfo}
23 |
24 | )}
25 | {successInfo && (
26 |
27 | {successInfo}
28 |
29 | )}
30 |
40 |
41 | {!loading && (
42 |
43 | Update
44 |
45 | )}
46 | {loading && (
47 |
48 |
55 | Loading...
56 |
57 | )}
58 |
59 |
60 | );
61 | };
62 |
63 | export default AvatarEdit;
64 |
--------------------------------------------------------------------------------
/src/components/profile/update-avatar/UpdateAvatar.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import AvatarEdit from './AvatarEdit';
3 | import useUpdateAvatar from '../../../hooks/useUpdateAvatar';
4 | import saveToS3 from '../../../utils/saveToS3';
5 |
6 | const UpdateAvatar = ({ userId, preview, setPreview }) => {
7 | const [updateAvatar] = useUpdateAvatar();
8 | const [errorInfo, setErrorInfo] = useState('');
9 | const [successInfo, setSuccessInfo] = useState('');
10 | const [loading, setLoading] = useState(false);
11 |
12 | const onSubmit = async () => {
13 | setLoading(true);
14 |
15 | try {
16 | const photoId = `${userId}-avatar`;
17 | const buf = Buffer.from(preview.replace(/^data:image\/\w+;base64,/, ''), 'base64');
18 | const imageUrl = await saveToS3(photoId, buf);
19 | await updateAvatar({ url: imageUrl });
20 | setSuccessInfo('Avatar updated');
21 | setTimeout(() => { setSuccessInfo(''); }, 3000);
22 | } catch (e) {
23 | setErrorInfo(e.message);
24 | setTimeout(() => { setErrorInfo(''); }, 3000);
25 | }
26 | setLoading(false);
27 | };
28 |
29 | return (
30 |
38 | );
39 | };
40 |
41 | export default UpdateAvatar;
42 |
--------------------------------------------------------------------------------
/src/components/profile/user-collections/CollectionCard.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Card } from 'react-bootstrap';
3 | import { useHistory } from 'react-router-dom';
4 | import galleryIcon from '../../../img/galleryIcon.jpg';
5 | import '../../../index.css';
6 | import CollectionDropdownButton from '../../others/button/edit-collection-btn/CollectionDropdownButtonDotIcon';
7 | import DeleteCollectionModal from './DeleteCollectionModal';
8 | import EditCollectionModal from './edit-collection/EditCollectionModal';
9 |
10 | const INIT_COVER = galleryIcon;
11 |
12 | const CollectionCard = ({
13 | showEditButton, collection, allCollections, setAllCollections,
14 | }) => {
15 | const [showEditCollectionModal, setShowEditCollectionModal] = useState(false);
16 | const [showDeleteModal, setShowDeleteModal] = useState(false);
17 |
18 | const history = useHistory();
19 |
20 | const openCollection = (collectionId) => {
21 | history.push(`/collection/${collectionId}`);
22 | };
23 |
24 | return (
25 |
26 |
27 | { openCollection(collection.id); }}
30 | onKeyPress={() => openCollection(collection.id)}
31 | role="button"
32 | tabIndex="0"
33 | >
34 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {showEditButton && (
46 |
47 | {collection.title}
48 | (
49 | {collection.photoCount}
50 | )
51 |
52 | )}
53 | {!showEditButton && (
54 |
55 | {collection.title}
56 | (
57 | {collection.photoCount}
58 | )
59 |
60 | )}
61 |
62 | {showEditButton && (
63 |
67 | )}
68 |
75 |
82 |
83 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | export default CollectionCard;
91 |
--------------------------------------------------------------------------------
/src/components/profile/user-collections/DeleteCollectionModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Modal, Alert } from 'react-bootstrap';
3 | import useDeleteCollection from '../../../hooks/useDeleteCollection';
4 |
5 | const DeleteCollectionModal = ({
6 | collectionNow,
7 | allCollections,
8 | setAllCollections,
9 | showDeleteModal,
10 | setShowDeleteModal,
11 | }) => {
12 | const [errorInfo, setErrorInfo] = useState('');
13 | const [successInfo, setSuccessInfo] = useState('');
14 | const [deleteCollection] = useDeleteCollection();
15 |
16 | const deleteSingleCollection = async () => {
17 | try {
18 | await deleteCollection({ id: collectionNow.id });
19 | setSuccessInfo('Collection is deleted');
20 |
21 | const updatedAllCollections = allCollections.filter((obj) => obj.id !== collectionNow.id);
22 | setTimeout(() => {
23 | setSuccessInfo('');
24 | setShowDeleteModal(false);
25 | setAllCollections(updatedAllCollections);
26 | }, 2000);
27 | } catch (e) {
28 | setErrorInfo(e.message);
29 | setTimeout(() => { setErrorInfo(''); }, 3000);
30 | }
31 | };
32 |
33 | return (
34 |
35 |
setShowDeleteModal(false)}
38 | centered
39 | dialogClassName="modal-90w"
40 | aria-labelledby="example-custom-modal-styling-title"
41 | >
42 |
43 |
44 | Delete this collection?
45 |
46 |
47 | {errorInfo && (
48 |
49 | {errorInfo}
50 |
51 | )}
52 | {successInfo && (
53 |
54 | {successInfo}
55 |
56 | )}
57 |
58 | setShowDeleteModal(false)}>
59 | Close
60 |
61 | deleteSingleCollection()}>
62 |
63 | Delete
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default DeleteCollectionModal;
72 |
--------------------------------------------------------------------------------
/src/components/profile/user-collections/edit-collection/EditCollectionContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik } from 'formik';
3 | import { Alert } from 'react-bootstrap';
4 | import * as Yup from 'yup';
5 |
6 | import EditCollectionForm from './EditCollectionForm';
7 |
8 | const validationSchema = Yup.object().shape({
9 | title: Yup
10 | .string()
11 | .required('Title is required'),
12 | description: Yup
13 | .string()
14 | .max(50, 'Must be 50 characters or less'),
15 | });
16 |
17 | const EditCollectionContainer = ({
18 | initialValues, onSubmit, errorInfo, successInfo, loading,
19 | }) => (
20 |
21 | {errorInfo && (
22 |
23 | {errorInfo}
24 |
25 | )}
26 | {successInfo && (
27 |
28 | {successInfo}
29 |
30 | )}
31 |
36 | {({ handleSubmit }) => }
37 |
38 |
39 | );
40 |
41 | export default EditCollectionContainer;
42 |
--------------------------------------------------------------------------------
/src/components/profile/user-collections/edit-collection/EditCollectionForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button, Spinner } from 'react-bootstrap';
4 | import { Form } from 'formik';
5 |
6 | import TextInput from '../../../others/TextInput';
7 |
8 | const EditCollectionForm = ({ loading }) => (
9 |
48 | );
49 |
50 | export default EditCollectionForm;
51 |
--------------------------------------------------------------------------------
/src/components/profile/user-collections/edit-collection/EditCollectionModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Modal } from 'react-bootstrap';
3 | import useEditCollection from '../../../../hooks/useEditCollection';
4 | import EditCollectionContainer from './EditCollectionContainer';
5 |
6 | const EditCollectionModal = ({
7 | collectionNow,
8 | allCollections,
9 | setAllCollections,
10 | showEditCollectionModal,
11 | setShowEditCollectionModal,
12 | }) => {
13 | const [editCollection] = useEditCollection();
14 | const [loading, setLoading] = useState(false);
15 | const [errorInfo, setErrorInfo] = useState('');
16 | const [successInfo, setSuccessInfo] = useState('');
17 |
18 | const initialValues = {
19 | title: collectionNow.title,
20 | description: collectionNow.description || '',
21 | };
22 |
23 | const onSubmit = async (values) => {
24 | const variables = {
25 | collectionId: collectionNow.id,
26 | newTitle: values.title,
27 | newDescription: values.description,
28 | };
29 |
30 | const updatedCollectionNow = {
31 | ...collectionNow,
32 | title: values.title,
33 | description: values.description,
34 | };
35 | setLoading(true);
36 |
37 | try {
38 | await editCollection(variables);
39 | setSuccessInfo('Collection details updated');
40 |
41 | const updatedAllCollections = allCollections
42 | .map((obj) => (obj.id === collectionNow.id ? updatedCollectionNow : obj));
43 | setTimeout(() => {
44 | setSuccessInfo('');
45 | setAllCollections(updatedAllCollections);
46 | }, 2000);
47 | setTimeout(() => { setSuccessInfo(''); }, 3000);
48 | } catch (e) {
49 | setErrorInfo(e.message);
50 | setTimeout(() => { setErrorInfo(''); }, 3000);
51 | }
52 | setLoading(false);
53 | };
54 |
55 | return (
56 |
57 | setShowEditCollectionModal(false)}
60 | centered
61 | dialogClassName="modal-90w"
62 | aria-labelledby="example-custom-modal-styling-title"
63 | >
64 |
65 |
66 | Edit Collection
67 | {' '}
68 | {collectionNow.title}
69 |
70 |
71 |
72 |
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default EditCollectionModal;
86 |
--------------------------------------------------------------------------------
/src/components/search/SearchPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useLocation, withRouter } from 'react-router-dom';
3 | import queryString from 'query-string';
4 | import { css } from '@emotion/react';
5 | import BeatLoader from 'react-spinners/BeatLoader';
6 | import usePhotos from '../../hooks/usePhotos';
7 | import HomePhotoList from '../others/photo-list/HomePhotoList';
8 | import RelatedTagBar from '../others/RelatedTagBar';
9 | import NavSearchBar from '../others/NavSearchBar';
10 |
11 | const override = css`
12 | display: flex;
13 | justify-content: center;
14 | align-item: center;
15 | margin-top: 3rem;
16 | `;
17 |
18 | const SearchPage = () => {
19 | const [allPhotos, setAllPhotos] = useState();
20 | const [loading, setLoading] = useState(false);
21 | const location = useLocation();
22 | const parsed = queryString.parse(location.search);
23 | const userId = localStorage.getItem('userId');
24 |
25 | const variables = {
26 | searchKeyword: parsed.q,
27 | checkUserLike: userId,
28 | first: 20,
29 | };
30 |
31 | const { photos, fetchMore, hasNextPage } = usePhotos(variables);
32 |
33 | useEffect(() => {
34 | if (photos) {
35 | const temp = photos && photos.edges
36 | ? photos.edges.map((edge) => edge.node)
37 | : [];
38 |
39 | setAllPhotos(temp);
40 | setLoading(false);
41 | }
42 | }, [photos]);
43 |
44 | const clickFetchMore = () => {
45 | fetchMore();
46 | setLoading(true);
47 | };
48 |
49 | return (
50 |
51 |
52 |
53 |
Search:
54 |
55 |
56 |
57 |
58 |
59 |
60 | { !photos && allPhotos && (
) }
61 |
68 |
69 | );
70 | };
71 |
72 | export default withRouter(SearchPage);
73 |
--------------------------------------------------------------------------------
/src/components/search/SearchPagePhotoList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { css } from '@emotion/react';
3 | import PacmanLoader from 'react-spinners/PacmanLoader';
4 | import SearchPagePhotoListContainer from './SearchPagePhotoListContainer';
5 |
6 | const override = css`
7 | display: flex;
8 | justify-content: center;
9 | align-item: center;
10 | margin: 3rem;
11 | margin-bottom: 6rem;
12 | `;
13 |
14 | const SearchPagePhotoList = ({
15 | allPhotos, setAllPhotos, clickFetchMore, loading,
16 | }) => {
17 | if (allPhotos === undefined) {
18 | return (
19 |
22 | );
23 | }
24 |
25 | return (
26 |
27 |
33 |
34 | );
35 | };
36 |
37 | export default SearchPagePhotoList;
38 |
--------------------------------------------------------------------------------
/src/components/search/SearchPagePhotoListContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useHistory } from 'react-router-dom';
3 | import Masonry from 'react-masonry-css';
4 | import LoadMore from '../others/button/LoadMore';
5 | import SearchPhotoCard from './SearchPhotoCard';
6 | import useCreateAndLikePhoto from '../../hooks/useCreateAndLikePhoto';
7 | import useUnlikeAndDeletePhoto from '../../hooks/useUnlikeAndDeletePhoto';
8 |
9 | const breakpointColumnsObj = {
10 | default: 3,
11 | 800: 2,
12 | 500: 2,
13 | };
14 |
15 | const SearchPagePhotoListContainer = ({
16 | allPhotos, setAllPhotos, clickFetchMore, loading,
17 | }) => {
18 | const [createAndLikePhoto] = useCreateAndLikePhoto();
19 | const [unlikeAndDeletePhoto] = useUnlikeAndDeletePhoto();
20 | const history = useHistory();
21 | const token = localStorage.getItem('token');
22 |
23 | const likeSinglePhoto = async (photo) => {
24 | if (!token) {
25 | history.push('/signin');
26 | } else {
27 | const temp = allPhotos
28 | .map((obj) => (obj.downloadPage === photo.downloadPage
29 | ? { ...obj, isLiked: !obj.isLiked } : obj));
30 | setAllPhotos(temp);
31 | if (photo.isLiked) {
32 | await unlikeAndDeletePhoto({ url: photo.downloadPage });
33 | } else {
34 | const variables = {
35 | width: photo.width,
36 | height: photo.height,
37 | color: photo.color,
38 | tiny: photo.tiny,
39 | small: photo.small,
40 | large: photo.large,
41 | downloadPage: photo.downloadPage,
42 | creditWeb: photo.creditWeb,
43 | creditId: photo.creditId,
44 | photographer: photo.photographer,
45 | description: photo.description || '',
46 | tags: photo.tags,
47 | };
48 |
49 | await createAndLikePhoto(variables);
50 | }
51 | }
52 | };
53 |
54 | return (
55 |
56 |
57 |
62 | {allPhotos.map((photo) => (
63 |
68 | ))}
69 |
70 |
71 |
76 |
77 | );
78 | };
79 |
80 | export default SearchPagePhotoListContainer;
81 |
--------------------------------------------------------------------------------
/src/components/sign-in/SignIn.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useHistory } from 'react-router-dom';
3 | import useSignIn from '../../hooks/useSignIn';
4 | import SignInContainer from './SignInContainer';
5 |
6 | const initialValues = {
7 | email: '',
8 | password: '',
9 | };
10 |
11 | const SignIn = () => {
12 | const [signIn] = useSignIn();
13 | const history = useHistory();
14 | const [errorInfo, setErrorInfo] = useState('');
15 | const [loading, setLoading] = useState(false);
16 |
17 | const onSubmit = async (values) => {
18 | const { email, password } = values;
19 | setLoading(true);
20 | try {
21 | await signIn({ email, password });
22 | history.push('/');
23 | } catch (e) {
24 | setErrorInfo(e.message);
25 | setLoading(false);
26 | setTimeout(() => { setErrorInfo(''); }, 3000);
27 | }
28 | };
29 |
30 | return (
31 |
37 | );
38 | };
39 |
40 | export default SignIn;
41 |
--------------------------------------------------------------------------------
/src/components/sign-in/SignInContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik } from 'formik';
3 | import { Image, Alert } from 'react-bootstrap';
4 | import * as Yup from 'yup';
5 |
6 | import SignInForm from './SignInForm';
7 |
8 | import logo from '../../img/logo/logo2.svg';
9 |
10 | const validationSchema = Yup.object().shape({
11 | email: Yup
12 | .string()
13 | .email('Invalid email address')
14 | .required('Email is required'),
15 | password: Yup
16 | .string()
17 | .required('Password is required'),
18 | });
19 |
20 | const SignInContainer = ({
21 | initialValues, onSubmit, errorInfo, loading,
22 | }) => (
23 |
24 |
25 |
26 |
Login
27 |
28 |
29 |
30 |
31 |
32 | {errorInfo && (
33 |
34 | {errorInfo}
35 |
36 | )}
37 |
42 | {({ handleSubmit }) => }
43 |
44 |
45 | Don’t have an account?
46 |
Join
47 |
48 |
49 | );
50 |
51 | export default SignInContainer;
52 |
--------------------------------------------------------------------------------
/src/components/sign-in/SignInForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button, Spinner } from 'react-bootstrap';
4 | import { Form } from 'formik';
5 |
6 | import TextInput from '../others/TextInput';
7 |
8 | const SignInForm = ({ loading }) => (
9 |
48 | );
49 |
50 | export default SignInForm;
51 |
--------------------------------------------------------------------------------
/src/components/sign-up/SignUp.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useHistory } from 'react-router-dom';
3 | import useSignUp from '../../hooks/useSignUp';
4 | import SignUpContainer from './SignUpContainer';
5 |
6 | const initialValues = {
7 | email: '',
8 | username: '',
9 | password: '',
10 | confirmPassword: '',
11 | };
12 |
13 | const SignUp = () => {
14 | const [signUp] = useSignUp();
15 | const history = useHistory();
16 | const [errorInfo, setErrorInfo] = useState('');
17 | const [loading, setLoading] = useState(false);
18 |
19 | const onSubmit = async (values) => {
20 | const variables = {
21 | firstName: values.username,
22 | email: values.email,
23 | username: values.username,
24 | password: values.password,
25 | };
26 | setLoading(true);
27 | try {
28 | await signUp(variables);
29 | history.push('/signin');
30 | } catch (e) {
31 | setErrorInfo(e.message);
32 | setLoading(false);
33 | setTimeout(() => { setErrorInfo(''); }, 3000);
34 | }
35 | };
36 |
37 | return (
38 |
44 | );
45 | };
46 |
47 | export default SignUp;
48 |
--------------------------------------------------------------------------------
/src/components/sign-up/SignUpContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image, Alert } from 'react-bootstrap';
3 | import { Formik } from 'formik';
4 | import * as Yup from 'yup';
5 |
6 | import SignUpForm from './SignUpForm';
7 | import logo from '../../img/logo/logo2.svg';
8 |
9 | const validationSchema = Yup.object({
10 | email: Yup.string()
11 | .email('Invalid email address')
12 | .required('Required'),
13 | username: Yup.string()
14 | .max(50, 'Must be 50 characters or less')
15 | .matches('^[a-zA-Z0-9_]*$', 'Invalid username')
16 | .required('Required'),
17 | password: Yup.string()
18 | .min(8, 'Password length must be greater than or equal to 8')
19 | .required('Required'),
20 | confirmPassword: Yup.string()
21 | .required()
22 | .oneOf([Yup.ref('password'), null], 'Passwords must match'),
23 | });
24 |
25 | const SignUpContainer = ({
26 | initialValues, onSubmit, errorInfo, loading,
27 | }) => (
28 |
29 |
30 |
31 |
Sign up
32 |
33 |
34 |
35 |
36 |
37 |
38 | Already have an account?
39 |
Login
40 |
41 | {errorInfo && (
42 |
43 | {errorInfo}
44 |
45 | )}
46 |
51 | {({ handleSubmit }) => }
52 |
53 |
54 | );
55 |
56 | export default SignUpContainer;
57 |
--------------------------------------------------------------------------------
/src/components/sign-up/SignUpForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button, Spinner } from 'react-bootstrap';
4 | import { Form } from 'formik';
5 |
6 | import TextInput from '../others/TextInput';
7 |
8 | const SignUpForm = ({ loading }) => (
9 |
70 | );
71 |
72 | export default SignUpForm;
73 |
--------------------------------------------------------------------------------
/src/components/upload/uploadComponent.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-nested-ternary */
2 | import React, { useEffect } from 'react';
3 | import { useDropzone } from 'react-dropzone';
4 |
5 | const thumbContainer = {
6 | display: 'flex',
7 | flexDirection: 'row',
8 | flexWrap: 'wrap',
9 | marginTop: 16,
10 | };
11 |
12 | const thumb = {
13 | display: 'inline-flex',
14 | borderRadius: 2,
15 | border: '1px solid #eaeaea',
16 | marginBottom: 8,
17 | marginRight: 8,
18 | maxWidth: 450,
19 | maxHeight: 300,
20 | padding: 4,
21 | boxSizing: 'border-box',
22 | };
23 |
24 | const thumbInner = {
25 | display: 'flex',
26 | minWidth: 0,
27 | overflow: 'hidden',
28 | };
29 |
30 | const img = {
31 | display: 'block',
32 | width: 'auto',
33 | height: '100%',
34 | };
35 |
36 | const baseStyle = {
37 | flex: 1,
38 | display: 'flex',
39 | flexDirection: 'column',
40 | alignItems: 'center',
41 | padding: '20px',
42 | borderWidth: 2,
43 | borderRadius: 2,
44 | borderColor: '#eeeeee',
45 | borderStyle: 'dashed',
46 | backgroundColor: '#fafafa',
47 | color: '#bdbdbd',
48 | outline: 'none',
49 | transition: 'border .24s ease-in-out',
50 | };
51 |
52 | const activeStyle = {
53 | borderColor: '#2196f3',
54 | };
55 |
56 | const acceptStyle = {
57 | borderColor: '#00e676',
58 | };
59 |
60 | const rejectStyle = {
61 | borderColor: '#ff1744',
62 | };
63 |
64 | function Previews({ files, setFiles }) {
65 | // const [files, setFiles] = useState([]);
66 | if (!files) return null;
67 | const {
68 | getRootProps,
69 | getInputProps,
70 | isDragActive,
71 | isDragAccept,
72 | isDragReject,
73 | } = useDropzone({
74 | // accept: "image/*",
75 | onDrop: (acceptedFiles) => {
76 | // console.log('accepted', acceptedFiles);
77 | setFiles(
78 | acceptedFiles.map((file) => Object.assign(file, { preview: URL.createObjectURL(file) })),
79 | );
80 | },
81 | });
82 |
83 | const style = React.useMemo(
84 | () => ({
85 | ...baseStyle,
86 | ...(isDragActive ? activeStyle : {}),
87 | ...(isDragAccept ? acceptStyle : {}),
88 | ...(isDragReject ? rejectStyle : {}),
89 | }),
90 | [isDragActive, isDragReject, isDragAccept],
91 | );
92 |
93 | const thumbs = files.map((file) => (
94 |
95 |
96 |
97 |
98 |
99 | ));
100 |
101 | useEffect(
102 | () => () => {
103 | // Make sure to revoke the data uris to avoid memory leaks
104 | files.forEach((file) => URL.revokeObjectURL(file.preview));
105 | },
106 | [files],
107 | );
108 |
109 | return (
110 |
111 |
112 |
113 | { isDragActive ? (
114 |
Drop the files here ...
115 | ) : files.length === 0 ? (
116 |
Drag and drop some files here, or click to select files
117 | ) : null}
118 |
119 |
120 |
121 | );
122 | }
123 |
124 | export default Previews;
125 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | import path from 'path';
3 |
4 | dotenv.config({ path: path.resolve(__dirname, '..', '/.env') });
5 |
6 | export default {
7 | port: process.env.REACT_APP_PORT,
8 | graphqlUri: process.env.REACT_APP_GRAPHQL_URI,
9 | philoartAdmin: process.env.REACT_APP_PHILOART_ADMIN,
10 | visitorID: process.env.REACT_APP_PHILOART_VISITOR,
11 | restApi: process.env.REACT_APP_PHILOART_API,
12 | moralisAppID: process.env.REACT_APP_MORALIS_APPLICATION_ID,
13 | moralisServerUrl: process.env.REACT_APP_MORALIS_SERVER_URL,
14 | };
15 |
--------------------------------------------------------------------------------
/src/graphql/fragment.js:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/react-hooks';
2 |
3 | export const PHOTO_DETAILS = gql`
4 | fragment photoDetails on Photo {
5 | id
6 | title
7 | year
8 | description
9 | tags
10 | srcTiny
11 | srcSmall
12 | srcLarge
13 | srcOriginal
14 | srcYoutube
15 | color
16 | allColors
17 | license
18 | type
19 | status
20 | allowDownload
21 | createdAt
22 | }
23 | `;
24 |
25 | export const COLLECTION_DETAILS = gql`
26 | fragment collectionDetails on Collection {
27 | id
28 | title
29 | description
30 | photoCount
31 | cover
32 | }
33 | `;
34 |
35 | export const USER_DETAILS = gql`
36 | fragment userDetails on User {
37 | id
38 | username
39 | firstName
40 | lastName
41 | email
42 | profileImage
43 | description
44 | }
45 | `;
46 |
47 | export default PHOTO_DETAILS;
48 |
--------------------------------------------------------------------------------
/src/hooks/useAuthorizedUser.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { GET_AUTHORIZED_USER } from '../graphql/queries';
4 |
5 | const useAuthorizedUser = () => {
6 | const {
7 | data, loading, ...result
8 | } = useQuery(GET_AUTHORIZED_USER, {
9 | fetchPolicy: 'cache-and-network',
10 | });
11 |
12 | return {
13 | authorizedUser: data ? data.authorizedUser : undefined,
14 | loading,
15 | ...result,
16 | };
17 | };
18 |
19 | export default useAuthorizedUser;
20 |
--------------------------------------------------------------------------------
/src/hooks/useChangePassword.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { CHANGE_PASSWORD } from '../graphql/mutations';
4 |
5 | const useChangePassword = () => {
6 | const [mutate, result] = useMutation(CHANGE_PASSWORD);
7 |
8 | const changePassword = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [changePassword, result];
15 | };
16 |
17 | export default useChangePassword;
18 |
--------------------------------------------------------------------------------
/src/hooks/useCollectPhoto.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { COLLECT_PHOTO } from '../graphql/mutations';
4 |
5 | const useCollectPhoto = () => {
6 | const [mutate, result] = useMutation(COLLECT_PHOTO);
7 |
8 | const collectPhoto = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [collectPhoto, result];
15 | };
16 |
17 | export default useCollectPhoto;
18 |
--------------------------------------------------------------------------------
/src/hooks/useCollection.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { GET_COLLECTION } from '../graphql/queries';
4 |
5 | const useCollection = (variables) => {
6 | const {
7 | data, fetchMore, refetch, loading, ...result
8 | } = useQuery(GET_COLLECTION, {
9 | fetchPolicy: 'cache-and-network',
10 | variables,
11 | });
12 |
13 | const handleFetchMore = () => {
14 | const canFetchMore = !loading && data && data.collection.photos.pageInfo.hasNextPage;
15 |
16 | if (!canFetchMore) {
17 | return;
18 | }
19 |
20 | fetchMore({
21 | query: GET_COLLECTION,
22 | variables: {
23 | after: data.collection.photos.pageInfo.endCursor,
24 | ...variables,
25 | },
26 | updateQuery: (previousResult, { fetchMoreResult }) => {
27 | const updatedPhotos = {
28 | ...fetchMoreResult.collection.photos,
29 | edges: [
30 | ...previousResult.collection.photos.edges,
31 | ...fetchMoreResult.collection.photos.edges,
32 | ],
33 | };
34 | const nextResult = {
35 | collection: {
36 | ...previousResult.collection,
37 | photos: updatedPhotos,
38 | },
39 | };
40 |
41 | return nextResult;
42 | },
43 | });
44 | };
45 |
46 | return {
47 | collection: data ? data.collection : undefined,
48 | fetchMore: handleFetchMore,
49 | hasNextPage: data && data.collection.photos.pageInfo.hasNextPage,
50 | refetch,
51 | loading,
52 | ...result,
53 | };
54 | };
55 |
56 | export default useCollection;
57 |
--------------------------------------------------------------------------------
/src/hooks/useCollections.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { GET_COLLECTIONS } from '../graphql/queries';
4 |
5 | const useCollections = (variables) => {
6 | const {
7 | data, fetchMore, refetch, loading, ...result
8 | } = useQuery(GET_COLLECTIONS, {
9 | fetchPolicy: 'cache-and-network',
10 | variables,
11 | });
12 |
13 | const handleFetchMore = () => {
14 | const canFetchMore = !loading && data && data.collections.pageInfo.hasNextPage;
15 |
16 | if (!canFetchMore) {
17 | return;
18 | }
19 |
20 | fetchMore({
21 | query: GET_COLLECTIONS,
22 | variables: {
23 | after: data.collections.pageInfo.endCursor,
24 | ...variables,
25 | },
26 | updateQuery: (previousResult, { fetchMoreResult }) => {
27 | const nextResult = {
28 | collections: {
29 | ...fetchMoreResult.collections,
30 | edges: [
31 | ...previousResult.collections.edges,
32 | ...fetchMoreResult.collections.edges,
33 | ],
34 | },
35 | };
36 |
37 | return nextResult;
38 | },
39 | });
40 | };
41 |
42 | return {
43 | collections: data ? data.collections : undefined,
44 | fetchMore: handleFetchMore,
45 | hasNextPage: data && data.collections.pageInfo.hasNextPage,
46 | refetch,
47 | loading,
48 | ...result,
49 | };
50 | };
51 |
52 | export default useCollections;
53 |
--------------------------------------------------------------------------------
/src/hooks/useCreateAndLikePhoto.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { CREATE_AND_LIKE_PHOTO } from '../graphql/mutations';
4 |
5 | const useCreateAndLikePhoto = () => {
6 | const [mutate, result] = useMutation(CREATE_AND_LIKE_PHOTO);
7 |
8 | const createAndLikePhoto = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [createAndLikePhoto, result];
15 | };
16 |
17 | export default useCreateAndLikePhoto;
18 |
--------------------------------------------------------------------------------
/src/hooks/useCreateCollection.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { CREATE_COLLECTION } from '../graphql/mutations';
4 |
5 | const useCreateCollection = () => {
6 | const [mutate, result] = useMutation(CREATE_COLLECTION);
7 |
8 | const createCollection = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [createCollection, result];
15 | };
16 |
17 | export default useCreateCollection;
18 |
--------------------------------------------------------------------------------
/src/hooks/useCreateCollectionAndCollectPhoto.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { CREATE_COLLECTION_AND_COLLECT_PHOTO } from '../graphql/mutations';
4 |
5 | const useCreateCollectionAndCollectPhoto = () => {
6 | const [mutate, result] = useMutation(CREATE_COLLECTION_AND_COLLECT_PHOTO);
7 |
8 | const createCollectionAndCollectPhoto = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 | let newCollection = null;
14 | if (result.data) {
15 | newCollection = result.data.createCollectionAndCollectPhoto.collection;
16 | }
17 |
18 | return [createCollectionAndCollectPhoto, newCollection];
19 | };
20 |
21 | export default useCreateCollectionAndCollectPhoto;
22 |
--------------------------------------------------------------------------------
/src/hooks/useCreatePhoto.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { CREATE_PHOTO } from '../graphql/mutations';
4 |
5 | const useCreatePhoto = () => {
6 | const [mutate, result] = useMutation(CREATE_PHOTO);
7 |
8 | const createPhoto = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [createPhoto, result];
15 | };
16 |
17 | export default useCreatePhoto;
18 |
--------------------------------------------------------------------------------
/src/hooks/useDeleteCollection.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { DELETE_COLLECTION } from '../graphql/mutations';
4 |
5 | const useDeleteCollection = () => {
6 | const [mutate, result] = useMutation(DELETE_COLLECTION);
7 |
8 | const deleteCollection = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [deleteCollection, result];
15 | };
16 |
17 | export default useDeleteCollection;
18 |
--------------------------------------------------------------------------------
/src/hooks/useDeletePhoto.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { DELETE_PHOTO } from '../graphql/mutations';
4 |
5 | const useDeletePhoto = () => {
6 | const [mutate, result] = useMutation(DELETE_PHOTO);
7 |
8 | const deletePhoto = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [deletePhoto, result];
15 | };
16 |
17 | export default useDeletePhoto;
18 |
--------------------------------------------------------------------------------
/src/hooks/useDeleteUser.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { DELETE_USER } from '../graphql/mutations';
4 |
5 | const useDeleteUser = () => {
6 | const [mutate, result] = useMutation(DELETE_USER);
7 |
8 | const deleteUser = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [deleteUser, result];
15 | };
16 |
17 | export default useDeleteUser;
18 |
--------------------------------------------------------------------------------
/src/hooks/useDiscoverCollections.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { GET_DISCOVER_COLLECTIONS } from '../graphql/queries';
4 |
5 | const useDiscoverCollections = (variables) => {
6 | const {
7 | data, loading, ...result
8 | } = useQuery(GET_DISCOVER_COLLECTIONS, {
9 | fetchPolicy: 'cache-and-network',
10 | variables,
11 | });
12 |
13 | return {
14 | collections: data ? data.collections : undefined,
15 | loading,
16 | ...result,
17 | };
18 | };
19 |
20 | export default useDiscoverCollections;
21 |
--------------------------------------------------------------------------------
/src/hooks/useDownloadPhoto.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { DOWNLOAD_PHOTO } from '../graphql/mutations';
4 |
5 | const useDownloadPhoto = () => {
6 | const [mutate, result] = useMutation(DOWNLOAD_PHOTO);
7 |
8 | const downloadPhoto = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [downloadPhoto, result];
15 | };
16 |
17 | export default useDownloadPhoto;
18 |
--------------------------------------------------------------------------------
/src/hooks/useEditCollection.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { EDIT_COLLECTION } from '../graphql/mutations';
4 |
5 | const useEditCollection = () => {
6 | const [mutate, result] = useMutation(EDIT_COLLECTION);
7 |
8 | const editCollection = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [editCollection, result];
15 | };
16 |
17 | export default useEditCollection;
18 |
--------------------------------------------------------------------------------
/src/hooks/useEditPhotoLabels.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { EDIT_PHOTO_LABELS } from '../graphql/mutations';
4 |
5 | const useEditPhotoLabels = () => {
6 | const [mutate, result] = useMutation(EDIT_PHOTO_LABELS);
7 |
8 | const editPhotoLabels = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [editPhotoLabels, result];
15 | };
16 |
17 | export default useEditPhotoLabels;
18 |
--------------------------------------------------------------------------------
/src/hooks/useField.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 | import { useState } from 'react';
3 |
4 | const useField = (type) => {
5 | const [value, setValue] = useState('');
6 |
7 | const onChange = (event) => {
8 | setValue(event.target.value);
9 | };
10 |
11 | return {
12 | type,
13 | value,
14 | onChange,
15 | };
16 | };
17 |
18 | export default useField;
19 |
--------------------------------------------------------------------------------
/src/hooks/useFollowUser.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { FOLLOW_USER } from '../graphql/mutations';
4 |
5 | const useFollowUser = () => {
6 | const [mutate, result] = useMutation(FOLLOW_USER);
7 |
8 | const followUser = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [followUser, result];
15 | };
16 |
17 | export default useFollowUser;
18 |
--------------------------------------------------------------------------------
/src/hooks/useLikePhoto.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { LIKE_PHOTO } from '../graphql/mutations';
4 |
5 | const useLikePhoto = () => {
6 | const [mutate, result] = useMutation(LIKE_PHOTO);
7 |
8 | const likePhoto = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [likePhoto, result];
15 | };
16 |
17 | export default useLikePhoto;
18 |
--------------------------------------------------------------------------------
/src/hooks/usePhoto.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { GET_PHOTO } from '../graphql/queries';
4 |
5 | const usePhoto = (variables) => {
6 | const {
7 | data, loading, ...result
8 | } = useQuery(GET_PHOTO, {
9 | fetchPolicy: 'cache-and-network',
10 | variables,
11 | });
12 |
13 | return {
14 | photo: data ? data.photo : undefined,
15 | loading,
16 | ...result,
17 | };
18 | };
19 |
20 | export default usePhoto;
21 |
--------------------------------------------------------------------------------
/src/hooks/usePhotos.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { GET_PHOTOS } from '../graphql/queries';
4 |
5 | const usePhotos = (variables) => {
6 | const {
7 | data, fetchMore, refetch, loading, ...result
8 | } = useQuery(GET_PHOTOS, {
9 | fetchPolicy: 'cache-and-network',
10 | variables,
11 | });
12 |
13 | const handleFetchMore = () => {
14 | const canFetchMore = !loading && data && data.photos.pageInfo.hasNextPage;
15 |
16 | if (!canFetchMore) {
17 | return;
18 | }
19 |
20 | fetchMore({
21 | query: GET_PHOTOS,
22 | variables: {
23 | after: data.photos.pageInfo.endCursor,
24 | ...variables,
25 | },
26 | updateQuery: (previousResult, { fetchMoreResult }) => {
27 | const nextResult = {
28 | photos: {
29 | ...fetchMoreResult.photos,
30 | edges: [
31 | ...previousResult.photos.edges,
32 | ...fetchMoreResult.photos.edges,
33 | ],
34 | },
35 | };
36 |
37 | return nextResult;
38 | },
39 | });
40 | };
41 |
42 | return {
43 | photos: data ? data.photos : undefined,
44 | fetchMore: handleFetchMore,
45 | hasNextPage: data && data.photos.pageInfo.hasNextPage,
46 | refetch,
47 | loading,
48 | ...result,
49 | };
50 | };
51 |
52 | export default usePhotos;
53 |
--------------------------------------------------------------------------------
/src/hooks/useSignIn.js:
--------------------------------------------------------------------------------
1 | import { useMutation, useApolloClient } from '@apollo/client';
2 |
3 | import { AUTHORIZE } from '../graphql/mutations';
4 |
5 | const useSignIn = () => {
6 | const [mutate, result] = useMutation(AUTHORIZE);
7 | const apolloClient = useApolloClient();
8 |
9 | const signIn = async ({ email, password }) => {
10 | const payload = await mutate({ variables: { email, password } });
11 | const { data } = payload;
12 |
13 | if (data && data.authorize) {
14 | localStorage.setItem('token', data.authorize.accessToken);
15 | localStorage.setItem('expirationTime', data.authorize.expiresAt);
16 | localStorage.setItem('userId', data.authorize.user.id);
17 | localStorage.setItem('username', data.authorize.user.username);
18 | apolloClient.resetStore();
19 | }
20 | return payload;
21 | };
22 |
23 | return [signIn, result];
24 | };
25 |
26 | export default useSignIn;
27 |
--------------------------------------------------------------------------------
/src/hooks/useSignUp.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { CREATE_USER } from '../graphql/mutations';
4 |
5 | const useSignUp = () => {
6 | const [mutate, result] = useMutation(CREATE_USER);
7 |
8 | const signUp = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [signUp, result];
15 | };
16 |
17 | export default useSignUp;
18 |
--------------------------------------------------------------------------------
/src/hooks/useUncollectPhoto.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { UNCOLLECT_PHOTO } from '../graphql/mutations';
4 |
5 | const useUncollectPhoto = () => {
6 | const [mutate, result] = useMutation(UNCOLLECT_PHOTO);
7 |
8 | const uncollectPhoto = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [uncollectPhoto, result];
15 | };
16 |
17 | export default useUncollectPhoto;
18 |
--------------------------------------------------------------------------------
/src/hooks/useUnfollowUser.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { UNFOLLOW_USER } from '../graphql/mutations';
4 |
5 | const useUnfollowUser = () => {
6 | const [mutate, result] = useMutation(UNFOLLOW_USER);
7 |
8 | const unfollowUser = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [unfollowUser, result];
15 | };
16 |
17 | export default useUnfollowUser;
18 |
--------------------------------------------------------------------------------
/src/hooks/useUnlikeAndDeletePhoto.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { UNLIKE_AND_DELETE_PHOTO } from '../graphql/mutations';
4 |
5 | const useUnlikeAndDeletePhoto = () => {
6 | const [mutate, result] = useMutation(UNLIKE_AND_DELETE_PHOTO);
7 |
8 | const unlikeAndDeletePhoto = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [unlikeAndDeletePhoto, result];
15 | };
16 |
17 | export default useUnlikeAndDeletePhoto;
18 |
--------------------------------------------------------------------------------
/src/hooks/useUnlikePhoto.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { UNLIKE_PHOTO } from '../graphql/mutations';
4 |
5 | const useUnlikePhoto = () => {
6 | const [mutate, result] = useMutation(UNLIKE_PHOTO);
7 |
8 | const unlikePhoto = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [unlikePhoto, result];
15 | };
16 |
17 | export default useUnlikePhoto;
18 |
--------------------------------------------------------------------------------
/src/hooks/useUpdateAvatar.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { UPDATE_AVATAR } from '../graphql/mutations';
4 |
5 | const useUpdateAvatar = () => {
6 | const [mutate, result] = useMutation(UPDATE_AVATAR);
7 |
8 | const updateAvatar = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [updateAvatar, result];
15 | };
16 |
17 | export default useUpdateAvatar;
18 |
--------------------------------------------------------------------------------
/src/hooks/useUpdateProfile.js:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 |
3 | import { UPDATE_PROFILE } from '../graphql/mutations';
4 |
5 | const useUpdateProfile = () => {
6 | const [mutate, result] = useMutation(UPDATE_PROFILE);
7 |
8 | const updateProfile = async (variables) => {
9 | await mutate({
10 | variables,
11 | });
12 | };
13 |
14 | return [updateProfile, result];
15 | };
16 |
17 | export default useUpdateProfile;
18 |
--------------------------------------------------------------------------------
/src/hooks/useUser.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { GET_USER } from '../graphql/queries';
4 |
5 | const useUser = (variables) => {
6 | const {
7 | data, loading, ...result
8 | } = useQuery(GET_USER, {
9 | fetchPolicy: 'cache-and-network',
10 | variables,
11 | });
12 |
13 | return {
14 | user: data ? data.user : undefined,
15 | loading,
16 | ...result,
17 | };
18 | };
19 |
20 | export default useUser;
21 |
--------------------------------------------------------------------------------
/src/hooks/useUserCollectionsPlus.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { GET_USER_COLLECTIONS_PLUS } from '../graphql/queries';
4 |
5 | const useCollections = (variables) => {
6 | const {
7 | data, fetchMore, refetch, loading, ...result
8 | } = useQuery(GET_USER_COLLECTIONS_PLUS, {
9 | fetchPolicy: 'cache-and-network',
10 | variables,
11 | });
12 |
13 | const handleFetchMore = () => {
14 | const canFetchMore = !loading && data && data.collections.pageInfo.hasNextPage;
15 |
16 | if (!canFetchMore) {
17 | return;
18 | }
19 |
20 | fetchMore({
21 | query: GET_USER_COLLECTIONS_PLUS,
22 | variables: {
23 | after: data.collections.pageInfo.endCursor,
24 | ...variables,
25 | },
26 | updateQuery: (previousResult, { fetchMoreResult }) => {
27 | const nextResult = {
28 | collections: {
29 | ...fetchMoreResult.collections,
30 | edges: [
31 | ...previousResult.collections.edges,
32 | ...fetchMoreResult.collections.edges,
33 | ],
34 | },
35 | };
36 |
37 | return nextResult;
38 | },
39 | });
40 | };
41 |
42 | return {
43 | collections: data ? data.collections : undefined,
44 | fetchMore: handleFetchMore,
45 | refetch,
46 | loading,
47 | ...result,
48 | };
49 | };
50 |
51 | export default useCollections;
52 |
--------------------------------------------------------------------------------
/src/hooks/useUserLikes.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { GET_USER_LIKES } from '../graphql/queries';
4 |
5 | const useUserLikedPhotos = (variables) => {
6 | const {
7 | data, fetchMore, refetch, loading, ...result
8 | } = useQuery(GET_USER_LIKES, {
9 | fetchPolicy: 'cache-and-network',
10 | variables,
11 | });
12 |
13 | const handleFetchMore = () => {
14 | const canFetchMore = !loading && data && data.likes.pageInfo.hasNextPage;
15 |
16 | if (!canFetchMore) {
17 | return;
18 | }
19 |
20 | fetchMore({
21 | query: GET_USER_LIKES,
22 | variables: {
23 | after: data.likes.pageInfo.endCursor,
24 | ...variables,
25 | },
26 | updateQuery: (previousResult, { fetchMoreResult }) => {
27 | const nextResult = {
28 | likes: {
29 | ...fetchMoreResult.likes,
30 | edges: [
31 | ...previousResult.likes.edges,
32 | ...fetchMoreResult.likes.edges,
33 | ],
34 | },
35 | };
36 |
37 | return nextResult;
38 | },
39 | });
40 | };
41 |
42 | return {
43 | likes: data ? data.likes : undefined,
44 | fetchMore: handleFetchMore,
45 | hasNextPage: data && data.likes.pageInfo.hasNextPage,
46 | refetch,
47 | loading,
48 | ...result,
49 | };
50 | };
51 |
52 | export default useUserLikedPhotos;
53 |
--------------------------------------------------------------------------------
/src/hooks/useUsers.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 |
3 | import { GET_USERS } from '../graphql/queries';
4 |
5 | const useUsers = (variables) => {
6 | const {
7 | data, fetchMore, refetch, loading, ...result
8 | } = useQuery(GET_USERS, {
9 | fetchPolicy: 'cache-and-network',
10 | variables,
11 | });
12 |
13 | const handleFetchMore = () => {
14 | const canFetchMore = !loading && data && data.users.pageInfo.hasNextPage;
15 |
16 | if (!canFetchMore) {
17 | return;
18 | }
19 |
20 | fetchMore({
21 | query: GET_USERS,
22 | variables: {
23 | after: data.users.pageInfo.endCursor,
24 | ...variables,
25 | },
26 | updateQuery: (previousResult, { fetchMoreResult }) => {
27 | const nextResult = {
28 | users: {
29 | ...fetchMoreResult.users,
30 | edges: [
31 | ...previousResult.users.edges,
32 | ...fetchMoreResult.users.edges,
33 | ],
34 | },
35 | };
36 |
37 | return nextResult;
38 | },
39 | });
40 | };
41 |
42 | return {
43 | users: data ? data.users : undefined,
44 | fetchMore: handleFetchMore,
45 | hasNextPage: data && data.users.pageInfo.hasNextPage,
46 | refetch,
47 | loading,
48 | ...result,
49 | };
50 | };
51 |
52 | export default useUsers;
53 |
--------------------------------------------------------------------------------
/src/hooks/useVerifyMetadata.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | // eslint-disable-next-line import/named
3 | import { useIPFS } from './useIPFS';
4 |
5 | /**
6 | * This is a hook that loads the NFT metadata in case it doesn't alreay exist
7 | * If metadata is missing, the object is replaced with a reactive object that
8 | * updatees when the data becomes available
9 | * The hook will retry until request is successful (with OpenSea, for now)
10 | */
11 | export const useVerifyMetadata = () => {
12 | const { resolveLink } = useIPFS();
13 | const [results, setResults] = useState({});
14 |
15 | /**
16 | * Fet Metadata from NFT and Cache Results
17 | * @param {object} NFT
18 | * @returns NFT
19 | */
20 | function verifyMetadata(NFT) {
21 | // Pass Through if Metadata already present
22 | if (NFT.metadata) return NFT;
23 | // Get the Metadata
24 | // eslint-disable-next-line no-use-before-define
25 | getMetadata(NFT);
26 | // Return Hooked NFT Object
27 | return results?.[NFT.token_uri] ? results?.[NFT.token_uri] : NFT;
28 | } // verifyMetadata()
29 |
30 | /**
31 | * Extract Metadata from NFT,
32 | * Fallback: Fetch from URI
33 | * @param {object} NFT
34 | * @returns void
35 | */
36 | async function getMetadata(NFT) {
37 | // Validate URI
38 | if (!NFT.token_uri || !NFT.token_uri.includes('://')) {
39 | console.log('getMetadata() Invalid URI', { URI: NFT.token_uri, NFT });
40 | return;
41 | }
42 | // Get Metadata
43 | fetch(NFT.token_uri)
44 | .then((res) => res.json())
45 | .then((metadata) => {
46 | if (!metadata) {
47 | // Log
48 | console.error(
49 | 'useVerifyMetadata.getMetadata() No Metadata found on URI:',
50 | { URI: NFT.token_uri, NFT },
51 | );
52 | // eslint-disable-next-line brace-style
53 | }
54 | // Handle Setbacks
55 | else if (
56 | metadata?.detail
57 | && metadata.detail.includes('Request was throttled')
58 | ) {
59 | // Log
60 | console.warn(
61 | `useVerifyMetadata.getMetadata() Bad Result for:${
62 | NFT.token_uri
63 | } Will retry later`,
64 | { results, metadata },
65 | );
66 | // Retry That Again after 1s
67 | setTimeout(() => {
68 | getMetadata(NFT);
69 | }, 1000);
70 | // eslint-disable-next-line brace-style
71 | } // Handle Opensea's {detail: "Request was throttled. Expected available in 1 second."}
72 | else {
73 | // No Errors
74 | // Set
75 | // eslint-disable-next-line no-use-before-define
76 | setMetadata(NFT, metadata);
77 | // Log
78 | console.log(
79 | `getMetadata() Late-load for NFT Metadata ${NFT.token_uri}`,
80 | { metadata },
81 | );
82 | } // Valid Result
83 | })
84 | .catch((err) => {
85 | console.error('useVerifyMetadata.getMetadata() Error Caught:', {
86 | err,
87 | NFT,
88 | URI: NFT.token_uri,
89 | });
90 | });
91 | } // getMetadata()
92 |
93 | /**
94 | * Update NFT Object
95 | * @param {object} NFT
96 | * @param {object} metadata
97 | */
98 | function setMetadata(NFT, metadata) {
99 | // Add Metadata
100 | // eslint-disable-next-line no-param-reassign
101 | NFT.metadata = metadata;
102 | // Set Image
103 | // eslint-disable-next-line no-param-reassign
104 | if (metadata?.image) NFT.image = resolveLink(metadata.image);
105 | // Set to State
106 | if (metadata && !results[NFT.token_uri]) { setResults({ ...results, [NFT.token_uri]: NFT }); }
107 | } // setMetadata()
108 |
109 | return { verifyMetadata };
110 | }; // useVerifyMetadata()
111 |
112 | export default useVerifyMetadata;
113 |
--------------------------------------------------------------------------------
/src/img/aboutImg4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/aboutImg4.jpg
--------------------------------------------------------------------------------
/src/img/galleryIcon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/galleryIcon.jpg
--------------------------------------------------------------------------------
/src/img/overlays/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/overlays/01.png
--------------------------------------------------------------------------------
/src/img/overlays/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/overlays/02.png
--------------------------------------------------------------------------------
/src/img/overlays/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/overlays/03.png
--------------------------------------------------------------------------------
/src/img/overlays/04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/overlays/04.png
--------------------------------------------------------------------------------
/src/img/overlays/05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/overlays/05.png
--------------------------------------------------------------------------------
/src/img/overlays/06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/overlays/06.png
--------------------------------------------------------------------------------
/src/img/overlays/07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/overlays/07.png
--------------------------------------------------------------------------------
/src/img/overlays/08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/overlays/08.png
--------------------------------------------------------------------------------
/src/img/overlays/09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/img/overlays/09.png
--------------------------------------------------------------------------------
/src/img/svg/arrow_left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/img/svg/arrow_right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {
4 | ApolloClient, ApolloProvider, HttpLink, InMemoryCache,
5 | } from '@apollo/client';
6 | import { setContext } from 'apollo-link-context';
7 | import config from './config';
8 |
9 | import App from './App';
10 |
11 | const authLink = setContext((_, { headers }) => {
12 | const token = localStorage.getItem('token');
13 | return {
14 | headers: {
15 | ...headers,
16 | authorization: token ? `bearer ${token}` : null,
17 | },
18 | };
19 | });
20 |
21 | const httpLink = new HttpLink({ uri: config.graphqlUri });
22 |
23 | const client = new ApolloClient({
24 | cache: new InMemoryCache(),
25 | link: authLink.concat(httpLink),
26 | });
27 |
28 | ReactDOM.render(
29 |
30 |
31 | ,
32 | document.getElementById('root'),
33 | );
34 |
--------------------------------------------------------------------------------
/src/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Philo-Li/philoart/a88b0442a8759e7a24a4739c0540001aa6750c29/src/logo.png
--------------------------------------------------------------------------------
/src/scss/about/about.scss:
--------------------------------------------------------------------------------
1 | .about .header {
2 | font-size: 3rem;
3 | font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
4 | color: #777;
5 | }
6 |
7 | .about .subheader {
8 | font-size: 2rem;
9 | font-weight: bold;
10 | font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11 | }
12 |
13 | .about .subheader2 {
14 | font-size: 1.5rem;
15 | font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16 | }
17 |
18 | .about .profile-details {
19 | font-size: 1rem;
20 | font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
21 | }
22 |
23 | .about {
24 | .about-footer {
25 | font-size: 1rem;
26 | font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
27 | }
28 |
29 | }
30 |
31 | .container-about {
32 | display: flex;
33 | flex-direction: row;
34 | justify-content: center;
35 | align-items: center;
36 | }
37 |
38 | .container-about-row-reverse {
39 | display: flex;
40 | flex-direction: row-reverse;
41 | justify-content: center;
42 | align-items: center;
43 | }
44 |
45 | .container-col-about-item {
46 | display: flex;
47 | flex-direction: column;
48 | justify-content: center;
49 | }
50 |
51 | .container-col-about {
52 | display: flex;
53 | flex-direction: column;
54 | justify-content: center;
55 | align-items: center;
56 | margin-top: 10%;
57 | margin-left: 10%;
58 | margin-right: 10%;
59 | }
60 |
61 | .container-row-about {
62 | display: flex;
63 | flex-direction: row;
64 | justify-content: center;
65 | align-items: center;
66 | margin-top: 10%;
67 | }
68 |
69 | container-col-about-icon {
70 | display: flex;
71 | flex-direction: column;
72 | justify-content: center;
73 | align-items: center;
74 | }
75 |
76 | @media screen and (max-width: 800px) {
77 | .about .header {
78 | font-size: 2rem;
79 | color: #777;
80 | }
81 |
82 | .about .subheader {
83 | font-size: 1.5rem;
84 | font-weight: bold;
85 | }
86 |
87 | .about .subheader2 {
88 | font-size: 1rem;
89 | }
90 |
91 | .about .icon-check {
92 | font-size: 1rem;
93 | }
94 | }
--------------------------------------------------------------------------------
/src/scss/collection-list/collection-list.scss:
--------------------------------------------------------------------------------
1 | .collection-dropbtn {
2 | background-color: white;
3 | color: black;
4 | width: auto;
5 | font-size: 16px;
6 | margin-right: 35px;
7 | border: none;
8 | cursor: pointer;
9 | }
10 |
11 | .user-collection-list-title-padding-left {
12 | flex-grow: 1;
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | padding-left: 2rem;
17 | }
18 |
19 | .user-collection-list-title {
20 | flex-grow: 1;
21 | display: flex;
22 | justify-content: center;
23 | align-items: center;
24 | }
25 |
26 | .user-collection-list-title-center {
27 | align-self: flex-end;
28 | justify-content: flex-end;
29 | }
30 |
31 | .user-collection-list-btn {
32 | align-self: flex-end;
33 | padding-right: 0.5rem;
34 | }
35 |
36 | .container-user-collection-list-title {
37 | display: flex;
38 | flex-flow: row;
39 | justify-content: center;
40 | align-items: center;
41 | padding-top: 0.5rem;
42 | }
--------------------------------------------------------------------------------
/src/scss/create/create.scss:
--------------------------------------------------------------------------------
1 | /* The container - needed to position the dropdown content */
2 | .license-btn .dropdown {
3 | position: relative;
4 | display: inline-block;
5 | }
6 |
7 | .license-btn .dropdown-content {
8 | display: none;
9 | position: absolute;
10 | width: 10rem;
11 | background-color: #f9f9f9;
12 | border-radius: 5px;
13 | z-index: 1;
14 | }
15 |
16 | .license-btn .dropdown-content button {
17 | display: block;
18 | background-color: white;
19 | width: 10rem;
20 | padding: 10px;
21 | font-size: 15px;
22 | border: 1px solid rgba(0, 0, 0, 0.25);
23 | border-radius: 5px;
24 | }
25 |
26 | .license-btn .dropdown-content button:hover {
27 | background-color: #f1f1f1;
28 | }
29 |
30 | .license-btn .dropdown:hover .dropdown-content {
31 | display: block;
32 | }
33 |
34 | .container-row-license {
35 | display: flex;
36 | flex-direction: row;
37 | justify-content: flex-start;
38 | }
39 |
40 | .container-row-license-item {
41 | align-self: flex-end;
42 | padding: 1rem;
43 | }
44 |
45 | .license-btn-info {
46 | font-size: 1rem;
47 | margin-top: 10%;
48 | font-weight: light;
49 | background-color: white;
50 | border: 1px solid black;
51 | border-radius: 5px;
52 | transition: ease 0.2s;
53 | }
54 |
55 | .container-row-license {
56 | display: flex;
57 | flex-direction: row;
58 | justify-content: flex-start;
59 | }
60 |
61 | .container-row-license-item {
62 | align-self: flex-end;
63 | padding: 1rem;
64 | }
65 |
66 | .license-btn-info {
67 | font-size: 1rem;
68 | margin-top: 10%;
69 | font-weight: light;
70 | background-color: white;
71 | border: 1px solid black;
72 | border-radius: 5px;
73 | transition: ease 0.2s;
74 | }
75 |
76 | .margin-tb-2rem {
77 | margin-top: 2rem;
78 | margin-bottom: 2rem;
79 | }
80 |
81 | .dropbtn {
82 | font-size: 1rem;
83 | margin-top: 10%;
84 | width: 10rem;
85 | font-weight: light;
86 | background-color: white;
87 | border: 1px solid black;
88 | border-radius: 5px;
89 | transition: ease 0.2s;
90 | }
--------------------------------------------------------------------------------
/src/scss/license/license.scss:
--------------------------------------------------------------------------------
1 | .container-col-license {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | margin-top: 5rem;
6 | margin-bottom: 5rem;
7 | margin-left: 5%;
8 | margin-right: 5%;
9 | }
10 |
11 | .license {
12 | background-image: url("https://images.unsplash.com/photo-1448375240586-882707db888b?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=1500&q=80");
13 | background-size: cover;
14 | height: 300px;
15 | display: flex;
16 | flex-direction: column;
17 | justify-content: center;
18 | align-items: center;
19 | }
20 |
21 | .license-header-bold-white {
22 | font-weight: bold;
23 | color: white;
24 | }
25 |
26 | .license-subheader {
27 | color: white;
28 | }
29 |
30 | .license-header-bold {
31 | font-weight: bold;
32 | }
33 |
34 | .license-icon-check {
35 | padding: 10px 20px;
36 | font-size: 2rem;
37 | color: cornflowerblue;
38 | width:50%;
39 | }
40 |
41 | .license-icon-x {
42 | padding: 10px 20px;
43 | font-size: 2rem;
44 | color: crimson;
45 | width:50%;
46 | }
47 |
48 | .license-msg-container {
49 | display: flex;
50 | align-items: center;
51 |
52 | }
53 |
54 | @media screen and (max-width: 800px) {
55 | .license-header-bold-white {
56 | font-size: 1.5rem;
57 | }
58 | .license-header-bold {
59 | font-size: 1.5rem;
60 | }
61 | .license-subheader {
62 | font-size: 1rem;
63 | }
64 | .license-subheader-footer {
65 | font-size: 1rem;
66 | }
67 | .license-msg {
68 | font-size: 1rem;
69 | }
70 | .license-icon-check {
71 | font-size: 1.5rem;
72 | }
73 | .license-icon-x {
74 | font-size: 1.5rem;
75 | }
76 | }
--------------------------------------------------------------------------------
/src/scss/navbar/navbar.scss:
--------------------------------------------------------------------------------
1 | /*forest green */
2 | .navbar-button-join {
3 | font-size: 18px;
4 | font-weight: 700;
5 | color: white;
6 | background-color: rgb(34,139,34);
7 | border: none;
8 | border-radius: 5px;
9 | padding: 2px 10px;
10 | transition: ease 0.2s;
11 | }
12 |
13 | .navbar-button-join:hover {
14 | color: white;
15 | background-color: rgb(76,187,23);
16 | /* -webkit-box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
17 | box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15); */
18 | }
19 |
20 | .navbar-button-logout {
21 | font-weight: light;
22 | color: black;
23 | background-color: transparent;
24 | border-radius: 5px;
25 | border-width: 1px;
26 | padding: 2px 10px;
27 | margin-right: 5px;
28 | transition: ease 0.2s;
29 | }
30 |
31 | .navbar-button-logout:hover {
32 | color: lightgrey;
33 | -webkit-box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
34 | box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
35 | }
36 |
37 | .navbar-link {
38 | font-weight: 500;
39 | color: black;
40 | background-color: transparent;
41 | border: none;
42 | padding: 8px 10px;
43 | transition: ease 0.2s;
44 | }
45 |
46 | .navbar-link:hover {
47 | color: lightgrey;
48 | }
49 |
50 | .container-row-navbar {
51 | display: flex;
52 | justify-content: center;
53 | align-items: center;
54 | }
55 |
56 | .container-row-navbar-brand {
57 | justify-content: center;
58 | align-items: center;
59 | display: flex;
60 | justify-content: center;
61 | align-items: center;
62 | font-weight: 500;
63 | color: black;
64 | }
65 |
66 | .container-row-navbar-searchbox {
67 | flex-grow: 1;
68 | border-color: black;
69 | border-width: 1px;
70 | border-radius: 5px;
71 | }
72 |
73 | .container-row-navbar-searchbox input[type=text] {
74 | font-size: 15px;
75 | width: 100%;
76 | padding: 8px 20px;
77 | }
78 |
79 | .container-row-searchpage-searchbox {
80 | border: 2px;
81 | }
82 |
83 | .container-row-searchpage-searchbox input[type=text] {
84 | border-radius: 5px;
85 | font-size: 20px;
86 | width: 100%;
87 | padding: 8px 20px;
88 | }
--------------------------------------------------------------------------------
/src/scss/photo-card/photo-card.scss:
--------------------------------------------------------------------------------
1 | .photo-card {
2 | position: relative;
3 | display: flex;
4 | align-items: flex-end;
5 | width:100%;
6 | height: 100%;
7 | }
8 |
9 | .photo-card-btn1 {
10 | position: absolute;
11 | text-align: center;
12 | margin-top: 30px;
13 | top: -2rem;
14 | right: .5rem;
15 | opacity: 0;
16 | transition: opacity .5s ease;
17 | }
18 |
19 | .photo-card-btn2 {
20 | position: absolute;
21 | text-align: center;
22 | margin-top: 30px;
23 | top: -2rem;
24 | right: 3rem;
25 | opacity: 0;
26 | transition: opacity .5s ease;
27 | }
28 |
29 | .photo-card-btn3 {
30 | position: absolute;
31 | text-align: center;
32 | margin-top: 30px;
33 | top: -2rem;
34 | right: 5.5rem;
35 | opacity: 0;
36 | transition: opacity .5s ease;
37 | }
38 |
39 | .photo-card-btn4 {
40 | position: absolute;
41 | text-align: center;
42 | margin-top: 30px;
43 | top: -2rem;
44 | right: 8rem;
45 | opacity: 0;
46 | transition: opacity .5s ease;
47 | }
48 |
49 | .photo-card-btn5 {
50 | position: absolute;
51 | text-align: start;
52 | margin-top: 30px;
53 | top: -2rem;
54 | right: 10.5rem;
55 | opacity: 0;
56 | transition: opacity .5s ease;
57 | }
58 |
59 | .photo-card-btn-web {
60 | position: absolute;
61 | text-align: center;
62 | margin-top: 30px;
63 | font-size: 1rem;
64 | top: -1.5rem;
65 | right: 5.5rem;
66 | opacity: 0;
67 | transition: opacity .5s ease;
68 | }
69 |
70 | .photo-card:hover .photo-card-btn1 {
71 | opacity: 1;
72 | }
73 |
74 | .photo-card:hover .photo-card-btn2 {
75 | opacity: 1;
76 | }
77 |
78 | .photo-card:hover .photo-card-btn3 {
79 | opacity: 1;
80 | }
81 |
82 | .photo-card:hover .photo-card-btn4 {
83 | opacity: 1;
84 | }
85 |
86 | .photo-card:hover .photo-card-btn5 {
87 | opacity: 1;
88 | }
89 |
90 | .photo-card:hover .photo-card-btn-web {
91 | opacity: 1;
92 | }
93 |
94 | .photo-card-img {
95 | position: relative;
96 | width: 100%;
97 | height: 100%;
98 | left: 0;
99 | }
100 |
101 | /* When the screen is less than 600px wide, make photo button visible */
102 | @media screen and (max-width: 600px) {
103 | .photo-card:hover .photo-card-btn-web {
104 | opacity: 0;
105 | }
106 |
107 | .photo-card-btn1 {
108 | font-size: 1.5rem;
109 | right: .5rem;
110 | opacity: 1;
111 | }
112 |
113 | .photo-card-btn2 {
114 | font-size: 1.5rem;
115 | right: 2.5rem;
116 | opacity: 1;
117 | }
118 |
119 | .photo-card-btn3 {
120 | font-size: 1.5rem;
121 | right: 4.5rem;
122 | opacity: 1;
123 | }
124 |
125 | .photo-card-btn4 {
126 | font-size: 1.5rem;
127 | right: 6.5rem;
128 | opacity: 0;
129 | }
130 |
131 | .photo-card:hover .photo-card-btn4 {
132 | opacity: 0;
133 | }
134 |
135 | .photo-card-btn5 {
136 | font-size: 1.5rem;
137 | right: 8.5rem;
138 | opacity: 0;
139 | }
140 |
141 | .photo-card:hover .photo-card-btn5 {
142 | opacity: 0;
143 | }
144 | }
145 |
146 | .photo-card-btn-icon {
147 | font-size: 1.5rem;
148 | margin-top: 10%;
149 | font-weight: bold;
150 | color: white;
151 | background-color: transparent;
152 | border: none;
153 | cursor: pointer;
154 | width: auto;
155 | height: auto;
156 | }
--------------------------------------------------------------------------------
/src/scss/photo-list/photo-list.scss:
--------------------------------------------------------------------------------
1 | .photo-list-container {
2 | display: flex;
3 | flex-direction: column;
4 | align-content: center;
5 | margin-top: 1rem;
6 | margin-bottom: 5rem;
7 | }
8 |
9 | .the-end {
10 | display: flex;
11 | flex-direction: column;
12 | align-content: center;
13 | }
14 |
15 | .the-end-title {
16 | display: flex;
17 | flex-direction: column;
18 | align-content: center;
19 | font-size: 1.2rem;
20 | font-weight: bold;
21 | color: #777;
22 | text-align-last: center;
23 | margin-top: 10%;
24 | margin-left: 15px;
25 | margin-right: 15px;
26 | background-color: white;
27 | border: 2px solid lightgray;
28 | padding: 0.5rem 1.5rem;
29 | transition: ease 0.2s;
30 | }
31 |
32 | .my-masonry-grid {
33 | display: -webkit-box; /* Not needed if autoprefixing */
34 | display: -ms-flexbox; /* Not needed if autoprefixing */
35 | display: flex;
36 | margin-left: -15px; /* gutter size offset */
37 | width: auto;
38 | }
39 | .my-masonry-grid_column {
40 | padding-left: 15px; /* gutter size */
41 | background-clip: padding-box;
42 | }
43 |
44 | /* Style your items */
45 | .my-masonry-grid_column > div { /* change div to reference your elements you put in */
46 | background: transparent;
47 | margin-bottom: 15px;
48 | }
--------------------------------------------------------------------------------
/src/scss/profile/profile.scss:
--------------------------------------------------------------------------------
1 | .button-follow {
2 | font-weight: light;
3 | color: white;
4 | background-color: rgba(0, 0, 0, 0.85);
5 | border-radius: 5px;
6 | border-width: 1px;
7 | padding: 2px 10px;
8 | margin-right: 5px;
9 | transition: ease 0.2s;
10 | }
11 |
12 | .button-follow:hover {
13 | color: lightgrey;
14 | -webkit-box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
15 | box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
16 | }
17 |
18 | .button-unfollow {
19 | font-weight: light;
20 | color: black;
21 | background-color: transparent;
22 | border-radius: 5px;
23 | border-width: 1px;
24 | padding: 2px 10px;
25 | margin-right: 5px;
26 | transition: ease 0.2s;
27 | }
28 |
29 | .button-unfollow:hover {
30 | color: lightgrey;
31 | -webkit-box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
32 | box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
33 | }
34 |
35 | .user-description {
36 | padding: 1rem;
37 | color:rgba(0, 0, 0, 0.75)
38 | }
39 |
40 | .button-follow:disabled {
41 | font-weight: light;
42 | color: black;
43 | background-color: rgba(0, 0, 0, 0.1);
44 | border-radius: 5px;
45 | border-width: 1px;
46 | padding: 2px 10px;
47 | margin-right: 5px;
48 | transition: ease 0.2s;
49 | }
50 |
51 | .discover-author-list a {
52 | color: black;
53 | }
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | .iiz {
2 | margin: 0;
3 | position: relative;
4 | overflow: hidden;
5 | display: inline-block;
6 | cursor: zoom-in;
7 | }
8 |
9 | .iiz--drag .iiz__zoom-img--visible {
10 | cursor: grab;
11 | }
12 |
13 | .iiz__img {
14 | max-width: 100%;
15 | height: auto;
16 | display: block;
17 | pointer-events: none;
18 | visibility: visible;
19 | opacity: 1;
20 | }
21 |
22 | .iiz__img--invisible {
23 | visibility: hidden;
24 | opacity: 0;
25 | }
26 |
27 | .iiz__zoom-img {
28 | width: auto !important;
29 | max-width: none !important;
30 | position: absolute;
31 | visibility: hidden;
32 | opacity: 0;
33 | display: block;
34 | }
35 |
36 | .iiz__zoom-img--visible {
37 | visibility: visible;
38 | opacity: 1;
39 | cursor: zoom-out;
40 | }
41 |
42 | .iiz__zoom-portal {
43 | position: fixed;
44 | top: 0;
45 | right: 0;
46 | bottom: 0;
47 | left: 0;
48 | z-index: 10000;
49 | }
50 |
51 | .iiz__btn {
52 | background: rgba(255, 255, 255, 0.8);
53 | width: 40px;
54 | height: 40px;
55 | border: none;
56 | outline: none;
57 | padding: 0;
58 | position: absolute;
59 | text-decoration: none;
60 | display: -webkit-box;
61 | display: -ms-flexbox;
62 | display: flex;
63 | -webkit-box-align: center;
64 | -ms-flex-align: center;
65 | align-items: center;
66 | -webkit-box-pack: center;
67 | -ms-flex-pack: center;
68 | justify-content: center;
69 | -webkit-appearance: none;
70 | appearance: none;
71 | }
72 |
73 | .iiz__btn:before {
74 | content: " ";
75 | background-position: center;
76 | background-repeat: no-repeat;
77 | display: block;
78 | }
79 |
80 | .iiz__hint {
81 | bottom: 10px;
82 | right: 10px;
83 | pointer-events: none;
84 | }
85 |
86 | .iiz__hint:before {
87 | content: " ";
88 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 19.9 19.9'%3E%3Cpath d='M13.9 7.4C13.9 3.8 11 .9 7.4.9S.9 3.8.9 7.4s2.9 6.5 6.5 6.5 6.5-2.9 6.5-6.5zm5.3 12.5l-6.7-7.2c-1.4 1.3-3.2 2.1-5.1 2.1-4.1 0-7.4-3.3-7.4-7.4S3.3 0 7.4 0s7.4 3.3 7.4 7.4c0 1.7-.6 3.4-1.7 4.7l6.8 7.2-.7.6z' fill='%23000222'/%3E%3C/svg%3E");
89 | width: 20px;
90 | height: 20px;
91 | }
92 |
93 | .iiz__close {
94 | top: 10px;
95 | right: 10px;
96 | visibility: hidden;
97 | opacity: 0;
98 | }
99 |
100 | .iiz__close--visible {
101 | visibility: visible;
102 | opacity: 1;
103 | }
104 |
105 | .iiz__close::before {
106 | content: " ";
107 | width: 29px;
108 | height: 29px;
109 | background-image: linear-gradient(#222, #222), linear-gradient(#222, #222);
110 | background-size: 100% 1px, 1px 100%;
111 | -webkit-transform: rotate(45deg);
112 | transform: rotate(45deg);
113 | }
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | const colors = {
2 | lighter: '#373940', // light blue
3 | dark: '#282c34', // dark blue
4 | darker: '#20232a', // really dark blue
5 | brand: '#61dafb', // electric blue
6 | brandLight: '#bbeffd',
7 | text: '#1a1a1a', // very dark grey / black substitute
8 | subtle: '#6d6d6d', // light grey for text
9 | subtleOnDark: '#999',
10 | divider: '#ececec', // very light grey
11 | note: '#ffe564', // yellow
12 | error: '#ff6464', // yellow
13 | white: '#ffffff',
14 | black: '#000000',
15 | };
16 |
17 | const fonts = {
18 | header: {
19 | fontSize: 60,
20 | lineHeight: '65px',
21 | fontWeight: 700,
22 | },
23 | small: {
24 | fontSize: 14,
25 | },
26 | };
27 |
28 | const SIZES = {
29 | xsmall: { min: 0, max: 599 },
30 | small: { min: 600, max: 779 },
31 | medium: { min: 780, max: 979 },
32 | large: { min: 980, max: 1279 },
33 | xlarge: { min: 1280, max: 1339 },
34 | xxlarge: { min: 1340, max: Infinity },
35 |
36 | // Sidebar/nav related tweakpoints
37 | largerSidebar: { min: 1100, max: 1339 },
38 | sidebarFixed: { min: 2000, max: Infinity },
39 |
40 | // Topbar related tweakpoints
41 | expandedSearch: { min: 1180, max: Infinity },
42 | };
43 |
44 | const media = {
45 | between(smallKey, largeKey, excludeLarge) {
46 | if (excludeLarge) {
47 | return `@media (min-width: ${
48 | SIZES[smallKey].min
49 | }px) and (max-width: ${SIZES[largeKey].min - 1}px)`;
50 | }
51 | if (SIZES[largeKey].max === Infinity) {
52 | return `@media (min-width: ${SIZES[smallKey].min}px)`;
53 | }
54 | return `@media (min-width: ${SIZES[smallKey].min}px) and (max-width: ${SIZES[largeKey].max}px)`;
55 | },
56 |
57 | greaterThan(key) {
58 | return `@media (min-width: ${SIZES[key].min}px)`;
59 | },
60 |
61 | lessThan(key) {
62 | return `@media (max-width: ${SIZES[key].min - 1}px)`;
63 | },
64 |
65 | size(key) {
66 | const size = SIZES[key];
67 |
68 | if (size.min == null) {
69 | return media.lessThan(key);
70 | } if (size.max == null) {
71 | return media.greaterThan(key);
72 | }
73 | return media.between(key, key);
74 | },
75 | };
76 |
77 | export { colors, fonts, media };
78 |
--------------------------------------------------------------------------------
/src/utils/formatters.js:
--------------------------------------------------------------------------------
1 | export const n6 = new Intl.NumberFormat('en-us', {
2 | style: 'decimal',
3 | minimumFractionDigits: 0,
4 | maximumFractionDigits: 6,
5 | });
6 | export const n4 = new Intl.NumberFormat('en-us', {
7 | style: 'decimal',
8 | minimumFractionDigits: 0,
9 | maximumFractionDigits: 4,
10 | });
11 |
12 | export const c2 = new Intl.NumberFormat('en-us', {
13 | style: 'currency',
14 | currency: 'USD',
15 | minimumFractionDigits: 2,
16 | maximumFractionDigits: 2,
17 | });
18 |
19 | /**
20 | * Returns a string of form "abc...xyz"
21 | * @param {string} str string to string
22 | * @param {number} n number of chars to keep at front/end
23 | * @returns {string}
24 | */
25 | export const getEllipsisTxt = (str, n = 6) => {
26 | if (str) {
27 | return `${str.slice(0, n)}...${str.slice(str.length - n)}`;
28 | }
29 | return '';
30 | };
31 |
32 | // eslint-disable-next-line no-restricted-properties
33 | export const tokenValue = (value, decimals) => (decimals ? value / Math.pow(10, decimals) : value);
34 |
35 | /**
36 | * Return a formatted string with the symbol at the end
37 | * @param {number} value integer value
38 | * @param {number} decimals number of decimals
39 | * @param {string} symbol token symbol
40 | * @returns {string}
41 | */
42 | export const tokenValueTxt = (value, decimals, symbol) => `${n4.format(tokenValue(value, decimals))} ${symbol}`;
43 |
--------------------------------------------------------------------------------
/src/utils/photos.json:
--------------------------------------------------------------------------------
1 | [{"node":{"id":"lGvXWWKSUuGpt6hQ4ctf-","title":"Untitled","srcTiny":"https://cdn.philoart.io/8/700x700/lGvXWWKSUuGpt6hQ4ctf-.jpg","srcSmall":"https://cdn.philoart.io/8/1200x1200/lGvXWWKSUuGpt6hQ4ctf-.jpg","srcLarge":"https://media.philoart.io/8/11-02-2014/lGvXWWKSUuGpt6hQ4ctf-.jpg","status":"unavailable"}},{"node":{"id":"CcqWNLZT9cmGsp_6S4bNQ","title":"Untitled","srcTiny":"https://cdn.philoart.io/1/700x700/CcqWNLZT9cmGsp_6S4bNQ.jpg","srcSmall":"https://cdn.philoart.io/1/1200x1200/CcqWNLZT9cmGsp_6S4bNQ.jpg","srcLarge":"https://media.philoart.io/1/11-02-2014/CcqWNLZT9cmGsp_6S4bNQ.jpg","status":"unavailable"}},{"node":{"id":"ejt2Vbza56UViZTf2vEHY","title":"Untitled","srcTiny":"https://cdn.philoart.io/b/700x700/ejt2Vbza56UViZTf2vEHY.jpg","srcSmall":"https://cdn.philoart.io/b/1200x1200/ejt2Vbza56UViZTf2vEHY.jpg","srcLarge":"https://media.philoart.io/b/11-02-2014/ejt2Vbza56UViZTf2vEHY.jpg","status":"unavailable"}},{"node":{"id":"GEaaaPxqtd-GCJX-oMoOi","title":"Untitled","srcTiny":"https://cdn.philoart.io/9/700x700/GEaaaPxqtd-GCJX-oMoOi.jpg","srcSmall":"https://cdn.philoart.io/9/1200x1200/GEaaaPxqtd-GCJX-oMoOi.jpg","srcLarge":"https://media.philoart.io/9/11-02-2014/GEaaaPxqtd-GCJX-oMoOi.jpg","status":"unavailable"}},{"node":{"id":"Ykf7fque1Rw3JJZ7TKSe5","title":"Untitled","srcTiny":"https://cdn.philoart.io/f/700x700/Ykf7fque1Rw3JJZ7TKSe5.jpg","srcSmall":"https://cdn.philoart.io/f/1200x1200/Ykf7fque1Rw3JJZ7TKSe5.jpg","srcLarge":"https://media.philoart.io/f/11-02-2014/Ykf7fque1Rw3JJZ7TKSe5.jpg","status":"unavailable"}},{"node":{"id":"6-aXEnfb2FlERVG8EDe16","title":"Untitled","srcTiny":"https://cdn.philoart.io/7/700x700/6-aXEnfb2FlERVG8EDe16.jpg","srcSmall":"https://cdn.philoart.io/7/1200x1200/6-aXEnfb2FlERVG8EDe16.jpg","srcLarge":"https://media.philoart.io/7/11-02-2014/6-aXEnfb2FlERVG8EDe16.jpg","status":"unavailable"}}]
--------------------------------------------------------------------------------
/src/utils/saveToS3.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import config from '../config';
3 |
4 | const baseUrl = config.restApi;
5 |
6 | const saveToS3 = async (photoId, buf) => {
7 | const { url } = await axios.get(`${baseUrl}/s3Url/${photoId}`).then((res) => res.data);
8 |
9 | // post the image direct to the s3 bucket
10 | await fetch(url, {
11 | method: 'PUT',
12 | headers: {
13 | 'Content-Type': 'image/jpeg',
14 | },
15 | ContentEncoding: 'base64',
16 | body: buf,
17 | });
18 |
19 | const imageUrl = url.split('?')[0];
20 | return imageUrl;
21 | };
22 |
23 | export default saveToS3;
24 |
--------------------------------------------------------------------------------