├── .github
└── workflows
│ └── master_mosaic-beta.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── SECURITY.md
├── azure-pipelines.yml
├── backend
├── api_docs.yml
├── application.py
└── templates
│ └── share.html
├── data_prep
├── download_images.py
└── featurize_and_match.py
├── developer_guide.md
├── frontend
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── manifest.json
│ └── mosaicTabLogo.png
├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── ExplorePage
│ │ ├── DefaultArtwork.tsx
│ │ ├── ExplorePage.tsx
│ │ ├── ListCarousel.tsx
│ │ ├── Options.tsx
│ │ ├── OverlayMap.tsx
│ │ ├── QueryArtwork.tsx
│ │ ├── ResultArtwork.tsx
│ │ └── SubmitControl.tsx
│ ├── IntroPage
│ │ ├── FairnessPage.tsx
│ │ └── IntroPage.tsx
│ ├── SearchPage
│ │ ├── ResultBox.tsx
│ │ ├── SearchControl.tsx
│ │ ├── SearchGrid.tsx
│ │ ├── SearchPage.tsx
│ │ └── TagList.tsx
│ ├── Shared
│ │ ├── AppInsights.tsx
│ │ ├── ArtSchemas.tsx
│ │ ├── BetaTools.tsx
│ │ ├── Constants.tsx
│ │ ├── NavBar.tsx
│ │ └── SearchTools.tsx
│ ├── fonts
│ │ ├── PlayfairDisplayBlack.ttf
│ │ └── PlayfairDisplayRegular.ttf
│ ├── images
│ │ ├── Rijks.jpg
│ │ ├── ajax-loader.gif
│ │ ├── banner5.jpg
│ │ ├── icon-checkbox-outline.svg
│ │ ├── icon-checkbox.svg
│ │ ├── icon-chevron-left-inverted.svg
│ │ ├── icon-chevron-left.svg
│ │ ├── icon-chevron-right-inverted.svg
│ │ ├── icon-chevron-right.svg
│ │ ├── icon-chevron.svg
│ │ ├── icon-clear.svg
│ │ ├── icon-earth.svg
│ │ ├── icon-less.svg
│ │ ├── icon-more.svg
│ │ ├── icon-search.svg
│ │ ├── mosaicLogo.png
│ │ └── the_met_logo_crop.png
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── main.scss
│ ├── react-app-env.d.ts
│ ├── serviceWorker.ts
│ └── styles
│ │ ├── abstracts
│ │ ├── _functions.scss
│ │ ├── _variables.scss
│ │ └── mixins
│ │ │ ├── _arrows.scss
│ │ │ ├── _aspect-ratio.scss
│ │ │ ├── _baseline.scss
│ │ │ ├── _container-centered.scss
│ │ │ ├── _crop-text.scss
│ │ │ ├── _custom-properties.scss
│ │ │ ├── _ellipsis-multiline.scss
│ │ │ ├── _ellipsis.scss
│ │ │ ├── _fonts.scss
│ │ │ ├── _modular-scale.scss
│ │ │ ├── _spacing.scss
│ │ │ ├── _transition.scss
│ │ │ └── _underline.scss
│ │ ├── base
│ │ ├── _base.scss
│ │ ├── _fonts.scss
│ │ ├── _typography.scss
│ │ └── _utilities.scss
│ │ ├── components
│ │ ├── _artwork-info.scss
│ │ ├── _btn.scss
│ │ ├── _burger.scss
│ │ ├── _explore.scss
│ │ ├── _gen-art.scss
│ │ ├── _grid-card.scss
│ │ ├── _intro.scss
│ │ ├── _nav.scss
│ │ ├── _original.scss
│ │ └── _search.scss
│ │ ├── layout
│ │ ├── _footer.scss
│ │ ├── _header.scss
│ │ └── _selectpage-head.scss
│ │ ├── pages
│ │ ├── _about.scss
│ │ ├── _explore.scss
│ │ └── _map.scss
│ │ └── vendor
│ │ ├── _normalize.scss
│ │ └── _slick.scss
├── tsconfig.json
└── utils
│ ├── 404.html
│ ├── head_additions.html
│ └── single_page_app.html
├── media
├── architecture.png
├── e2e.gif
├── header-image.jpg
├── match1.jpg
├── match2.jpg
├── mit_externs.jpg
├── mosaic.svg
├── teaser_img.gif
└── webinar.jpg
├── package-lock.json
└── transparency_note.md
/.github/workflows/master_mosaic-beta.yml:
--------------------------------------------------------------------------------
1 | # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
2 | # More GitHub Actions for Azure: https://github.com/Azure/actions
3 |
4 | name: Build and deploy Node.js app to Azure Web App - mosaic-beta
5 |
6 | on:
7 | push:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build-and-deploy:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@master
17 |
18 | - name: Set up Node.js version
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: '12.x'
22 |
23 | - name: npm install, build, and test
24 | run: |
25 | cd frontend
26 | npm install
27 | npm run build --if-present
28 | mv utils/404.html build/
29 |
30 | - name: 'Deploy to Azure Web App'
31 | uses: azure/webapps-deploy@v2
32 | with:
33 | app-name: 'mosaic-beta'
34 | slot-name: 'production'
35 | publish-profile: ${{ secrets.AzureAppService_PublishProfile_49db28a9807849198359c51c5330fea1 }}
36 | package: frontend/build
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | .idea/
132 | .vscode/
133 | .azure/
134 | .vs/
135 | outputs/
136 | *.ball
137 |
138 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
139 | .env
140 |
141 | # dependencies
142 | /node_modules
143 | /.pnp
144 | .pnp.js
145 |
146 | # testing
147 | /coverage
148 |
149 | # production
150 | /build
151 |
152 | # misc
153 | .DS_Store
154 | .env.local
155 | .env.development.local
156 | .env.test.local
157 | .env.production.local
158 |
159 | npm-debug.log*
160 | yarn-debug.log*
161 | yarn-error.log*
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
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 |
2 |
3 |
4 |
5 |
6 |
7 | ## [Live Demo at aka.ms/mosaic](https://aka.ms/mosaic)
8 |
9 | To access the search functionality, [apply to access the mosaic beta](https://forms.microsoft.com/Pages/DesignPage.aspx#FormId=v4j5cvGGr0GRqy180BHbR3nswihwe8JLvwovyYerymVUQlUzOE9VVDUyQjlJUzRFQ1pQUEJDN001Wi4u)
10 |
11 | ## About
12 |
13 | Art is one of the few languages which transcends barriers of country, culture, and time. We aim to create an algorithm that can help discover the common semantic elements of art even between **any** culture, media, artist, or collection within the combined artworks of [The Metropolitan Museum of Art](https://www.metmuseum.org/) and [The Rijksmusem](https://www.rijksmuseum.nl/en).
14 |
15 | ### Conditional Image Retrieval
16 |
17 |
18 |
19 |
20 |
21 | Image retrieval systems allow individuals to find images that are semantically similar to a query image. This serves as the backbone of reverse image search engines and many product recommendation engines.
22 | We present a novel method for specializing image retrieval systems called conditional image retrieval. When applied over large art datasets, conditional image retrieval provides visual analogies that bring to light hidden connections among different artists, cultures, and media. Conditional image retrieval systems can efficiently find shared semantics between works of vastly different media and cultural origin. [Our paper](https://arxiv.org/abs/2007.07177) introduces new variants of K-Nearest Neighbor algorithms that support specializing to particular subsets of image collections on the fly.
23 |
24 | ### Deep Semantic Similarity
25 |
26 | To find artworks with similar semantic structure we leverage "features" from deep vision networks trained on ImageNet. These networks map images into a high-dimensional space where distance is semantically meaningful. Here, nearest neighbor queries tend to act as "reverse image search engines" and similar objects often share common structure.
27 |
28 |
29 |
30 |
31 |
32 | ### Architecture
33 |
34 |
35 |
36 |
37 |
38 | ## Webinar
39 | To learn more about this project please join our [live webinar](https://note.microsoft.com/MSR-Webinar-Visual-Analogies-Registration-Live.html) on 10AM PST 7/30/2020.
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | ## Paper
48 |
49 | - Hamilton, M., Fu, S., Freeman, W. T., & Lu, M. (2020). Conditional Image Retrieval. arXiv preprint [arXiv:2007.07177](https://arxiv.org/abs/2007.07177).
50 |
51 | To cite this work please use the following:
52 | ```
53 | @article{hamilton2020conditional,
54 | title={Conditional Image Retrieval},
55 | author={Hamilton, Mark and Fu, Stephanie and Freeman, William T and Lu, Mindren},
56 | journal={arXiv preprint arXiv:2007.07177},
57 | year={2020}
58 | }
59 | ```
60 |
61 | ## Developer Guide
62 |
63 | Please see our [developer guide](./developer_guide.md) to build the project for yourself.
64 |
65 | ## Some Favorite Matches
66 |
67 | Shared portrayals of reverence over 3000 years:
68 |
69 |
70 |
71 |
72 |
73 | How to match your watch to your outfit and your dinnerware:
74 |
75 |
76 |
77 |
78 |
79 | ## Contributors
80 |
81 | Special thanks to all of the contributors who helped make this project a reality!
82 |
83 | #### Project Leads
84 | - [Mark Hamilton](https://mhamilton.net)
85 | - Chris Hoder
86 |
87 | #### Collaborators
88 | - [Professor William T Freeman](https://billf.mit.edu/)
89 | - [Lei Zhang](https://www.microsoft.com/en-us/research/people/leizhang/)
90 | - Anand Raman
91 | - Al Bracuti
92 | - Ryan Gaspar
93 | - Christina Lee
94 | - Lily Li
95 |
96 | #### MIT x MSFT Garage 2020 Externship Team:
97 |
98 |
99 |
100 |
101 |
102 | The MIT x MSFT externs were pivotal in turning this research project into a functioning website. In only one month, the team built and designed the mosaic website. Stephanie Fu and Mindren Lu also contributed to the "Conditional Image Retrieval" publication through their evaluation of the affect of different pre-trained networks on nonparametric style transfer.
103 | - Stephanie Fu
104 | - Mindren Lu
105 | - Zhenbang (Ben) Chen
106 | - Felix Tran
107 | - Darius Bopp
108 | - Margaret (Maggie) Wang
109 | - Marina Rogers
110 | - Johnny Bui
111 |
112 | #### MSFT Garage Staff and Mentors
113 | This project owes a heavy thanks to the MSFT Garage team. They are passionate creators who seek to incubate new projects and inspire new generations of engineers. Their support and mentorship on this project are sincerely appreciated.
114 | - Chris Templeman
115 | - Linda Thackery
116 | - Jean-Yves Ntamwemezi
117 | - Dalitso Banda
118 | - Anunaya Pandey
119 |
120 | ## Contributing
121 |
122 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
123 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
124 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
125 |
126 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide
127 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
128 | provided by the bot. You will only need to do this once across all repos using our CLA.
129 |
130 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
131 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
132 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
133 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | branches:
3 | include:
4 | - master
5 | paths:
6 | exclude:
7 | - '*.md'
8 |
9 | pool:
10 | vmImage: 'ubuntu-latest'
11 |
12 | steps:
13 | - task: UsePythonVersion@0
14 | inputs:
15 | versionSpec: '3.6'
16 | displayName: Setting Python Version...
17 | - script: cd azureml && pip install -r requirements.txt
18 | displayName: 'Installing dependencies...'
19 | - task: DockerInstaller@0
20 | inputs:
21 | dockerVersion: '17.09.0-ce'
22 | displayName: Installing Docker...
23 | - task: AzureCLI@2
24 | inputs:
25 | azureSubscription: 'MMLSpark Connection'
26 | scriptType: 'bash'
27 | scriptLocation: 'inlineScript'
28 | inlineScript: |
29 | python3 deploy_score_aks.py
30 | displayName: Deploying Inference Service...
31 | - script: python call_service.py
32 | displayName: Checking if inference service is online...
33 | - task: AzureCLI@2
34 | inputs:
35 | azureSubscription: 'MMLSpark Connection'
36 | scriptType: 'bash'
37 | scriptLocation: 'inlineScript'
38 | inlineScript: |
39 | cd ../backend
40 | az webapp up --name mosaicart
41 | displayName: Deploying Facebook Sharing Service...
--------------------------------------------------------------------------------
/backend/api_docs.yml:
--------------------------------------------------------------------------------
1 | swagger: '2.0'
2 | info:
3 | version: '1.0'
4 | title: "Deep Culture Explorer"
5 | description: The Deep Culture Explorer API allows users to explore similarities between artworks of different mediums and cultures.
6 | host: art-backend.azurewebsites.net
7 | basePath: /
8 | schemes:
9 | - http
10 | consumes:
11 | - application/json
12 | produces:
13 | - application/json
14 | paths:
15 | /select:
16 | x-summary: Select API
17 | get:
18 | summary: Get the Artwork Object
19 | description: Given an object ID and source museum, get the image object associated with that
20 | parameters:
21 | - name: id
22 | in: query
23 | description: the id of the artwork to search for
24 | type: string
25 | x-example: 'aa08df9c'
26 | required: true
27 | - name: museum
28 | in: query
29 | description: the museum name to search in
30 | type: string
31 | x-example: 'rijksmuseum'
32 | required: true
33 | responses:
34 | 200:
35 | description: Successful Response
36 | schema:
37 | $ref: '#/definitions/Artwork'
38 | examples:
39 | application/json:
40 | img_url: "https://example.com/aa08df9c.jpg"
41 | title: 'De Nachtwacht'
42 | museum: 'rijksmuseum'
43 | 404:
44 | description: Artwork Not Found
45 | /explore:
46 | x-summary: Explore API
47 | get:
48 | summary: Get Similar Artworks
49 | description: Get a list of similar artwork given a set of filters
50 | parameters:
51 | - name: url
52 | in: query
53 | description: the url of the image to find similar artwork to
54 | type: string
55 | x-example: 'https://example.com/oafd98fa.jpg'
56 | required: true
57 | - name: numResults
58 | in: query
59 | description: the number of results to return
60 | type: number
61 | x-example: 10
62 | required: true
63 | - name: culture
64 | in: query
65 | description: the culture filter to apply to the similarity search
66 | type: array
67 | items:
68 | type: string
69 | x-example: ["american", "russian"]
70 | - name: medium
71 | in: query
72 | description: the medium filter to apply to the similarity search
73 | type: array
74 | items:
75 | type: string
76 | x-example: ["pottery", "canvas"]
77 | responses:
78 | 200:
79 | description: Successful Response
80 | schema:
81 | type: array
82 | items:
83 | $ref: '#/definitions/Artwork'
84 | examples:
85 | application/json:
86 | - img_url: "https://example.com/df0vh929c.jpg"
87 | title: 'schilderijen'
88 | museum: 'rijksmuseum'
89 | - img_url: "https://example.com/aa08df9c.jpg"
90 | title: 'De Nachtwacht'
91 | museum: 'rijksmuseum'
92 | 404:
93 | description: Artwork Not Found
94 | /search:
95 | x-summary: Search API
96 | get:
97 | summary: Get Artwork with Text Search
98 | description: Given a text search and filters, return a list of artworks
99 | parameters:
100 | - name: query
101 | in: query
102 | description: the text query to search for
103 | type: string
104 | x-example: 'Mona Lisa'
105 | required: true
106 | responses:
107 | 200:
108 | description: Successful Response
109 | schema:
110 | type: array
111 | items:
112 | $ref: '#/definitions/Artwork'
113 | examples:
114 | application/json:
115 | - img_url: "https://example.com/df0vh929c.jpg"
116 | title: 'schilderijen'
117 | museum: 'rijksmuseum'
118 | - img_url: "https://example.com/aa08df9c.jpg"
119 | title: 'De Nachtwacht'
120 | museum: 'rijksmuseum'
121 | /curated:
122 | x-summary: Curated API
123 | get:
124 | summary: Get a Curated List of Artworks
125 | description: Return a Curated List of Artworks
126 | responses:
127 | 200:
128 | description: Successful Response
129 | schema:
130 | type: object
131 | additionalProperties:
132 | type: array
133 | items:
134 | $ref: '#/definitions/Artwork'
135 | examples:
136 | application/json:
137 | # type: object
138 | # properties:
139 | Italian:
140 | - img_url: "https://lh3.googleusercontent.com/mAyAjvYjIeAIlByhJx1Huctgeb58y7519XYP38oL1FXarhVlcXW7kxuwayOCFdnwtOp6B6F0HJmmws-Ceo5b_pNSSQs=s0"
141 | title: "Isaac and Rebecca, Known as ‘The Jewish Bride’"
142 | museum: "rijksmuseum"
143 | French:
144 | - img_url: "https://lh3.googleusercontent.com/AyiKhdEWJ7XmtPXQbRg_kWqKn6mCV07bsuUB01hJHjVVP-ZQFmzjTWt7JIWiQFZbb9l5tKFhVOspmco4lMwqwWImfgg=s0"
145 | title: "Portrait of a Woman, Possibly Maria Trip, Rembrandt van Rijn, 1639"
146 | museum: "rijksmuseum"
147 | definitions:
148 | ArtworkList:
149 | title: ArtworkList
150 | type: array
151 | items:
152 | type: object
153 | items:
154 | $ref: '#/definitions/Artwork'
155 | Artwork:
156 | title: Artwork
157 | type: object
158 | properties:
159 | img_url:
160 | type: string
161 | title:
162 | type: string
163 | museum:
164 | type: string
165 | required:
166 | - img_url
167 | - title
168 | - museum
169 | additionalProperties:
170 | type: string
171 | Filters:
172 | title: Filters
173 | type: object
174 | properties:
175 | museum:
176 | type: array
177 | items:
178 | type: string
179 | culture:
180 | type: array
181 | items:
182 | type: string
183 |
--------------------------------------------------------------------------------
/backend/application.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, redirect, url_for, request, render_template, jsonify, abort
2 | from werkzeug.utils import secure_filename
3 | import base64
4 |
5 | import os
6 | import subprocess
7 | import sys
8 |
9 | def install(package):
10 | subprocess.check_call([sys.executable, "-m", "pip", "install", package])
11 |
12 |
13 | install("azure-storage-blob")
14 | install("flask-cors")
15 | from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient, ContentSettings
16 | from flask_cors import CORS
17 |
18 | connect_str = os.getenv('AZURE_STORAGE_CONNECTION_STRING')
19 | blob_service_client = BlobServiceClient(
20 | account_url=connect_str
21 | )
22 | container_name = "mosaic-shares"
23 |
24 | BAD_REQUEST_STATUS_CODE = 400
25 | NOT_FOUND_STATUS_CODE = 404
26 |
27 |
28 | def allowed_file(filename):
29 | return '.' in filename and \
30 | filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg'}
31 |
32 |
33 | app = Flask(__name__)
34 | CORS(app)
35 |
36 |
37 | @app.route('/', methods=['GET'])
38 | def home():
39 | return jsonify({"status": "ok", "version": "1.0.0"})
40 |
41 |
42 | @app.route('/upload', methods=['POST'])
43 | def upload():
44 | # frontend uploads image, we save to azure storage blob and return a link to the image and the share page
45 | if request.method == 'POST':
46 | if request.args.get("filename") is None:
47 | return jsonify({"error": "filename parameter must be specified"})
48 | filename = request.args.get("filename")
49 | content_type = None
50 | try:
51 | img_b64 = request.form.get('image').split(',')
52 | image = base64.b64decode(img_b64[1])
53 | content_type = img_b64[0].split(':')[1].split(';')[0] # gets content type from data:image/png;base64
54 | except:
55 | return jsonify({"error": "unable to decode"})
56 |
57 | if allowed_file(filename):
58 | filename = secure_filename(filename)
59 | blob_client = blob_service_client.get_blob_client(container=container_name, blob=filename)
60 | try:
61 | blob_client.upload_blob(image)
62 | blob_client.set_http_headers(content_settings=ContentSettings(content_type=content_type))
63 | print(content_type)
64 | except Exception as err:
65 | print(err)
66 | finally:
67 | img_url = "https://mmlsparkdemo.blob.core.windows.net/mosaic-shares/mosaic-shares/" + filename
68 | return jsonify({"img_url": img_url})
69 | return jsonify({"error": "error processing file"})
70 | else:
71 | return jsonify({"error": "upload is a post request"})
72 |
73 |
74 | @app.route('/share', methods=['GET'])
75 | def share():
76 | image_url = request.args.get('image_url')
77 | title = request.args.get('title')
78 | description = request.args.get('description')
79 | redirect_url = request.args.get('redirect_url')
80 | width = request.args.get('width')
81 | height = request.args.get('height')
82 | # input param 'url', we return a page that can be shared with facebook (with correct opengraph tags)
83 | return render_template(
84 | "share.html",
85 | image_url=image_url,
86 | title=title,
87 | description=description,
88 | redirect_url=redirect_url,
89 | width=width,
90 | height=height
91 | )
92 |
93 |
94 | if __name__ == "__main__":
95 | app.run(debug=True, host='0.0.0.0')
96 |
--------------------------------------------------------------------------------
/backend/templates/share.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
22 |
23 |
24 |
25 | Make your own Mosaic here .
26 |
27 |
--------------------------------------------------------------------------------
/data_prep/download_images.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pandas as pd
4 | import requests
5 | from tqdm import tqdm
6 |
7 | batch_size = 256
8 | img_width = 225
9 | img_height = 225
10 | model = "resnet"
11 | metadata_fn = "metadata.json"
12 | data_dir = "images"
13 |
14 | os.makedirs(data_dir, exist_ok=True)
15 | metadata = pd.read_json(metadata_fn, lines=True)
16 | for i, row in tqdm(metadata.iterrows()):
17 | target_file = os.path.join(data_dir, row["id"] + ".jpg")
18 | if not os.path.exists(target_file):
19 | try:
20 | with open(target_file, 'wb') as f:
21 | f.write(requests.get(row["Thumbnail_Url"]).content)
22 | except Exception as e:
23 | print(e)
24 |
--------------------------------------------------------------------------------
/data_prep/featurize_and_match.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pandas as pd
4 | import torch
5 | import torch.nn as nn
6 | from PIL import Image
7 | from torch.utils.data import Dataset
8 | from torchvision import models, transforms
9 | from tqdm import tqdm
10 | import numpy as np
11 | import pickle
12 |
13 | data_dir = 'images'
14 | metadata_fn = "metadata.json"
15 | features_dir = "features"
16 | features_file = os.path.join(features_dir, "pytorch_rn50.pkl")
17 | featurize_images = True
18 | device = torch.device("cuda:0")
19 |
20 | os.makedirs(data_dir, exist_ok=True)
21 | os.makedirs(features_dir, exist_ok=True)
22 |
23 | if featurize_images:
24 | class ArtDataset(Dataset):
25 | """Face Landmarks dataset."""
26 |
27 | def __init__(self, metadata_json, image_dir, transform):
28 | """
29 | Args:
30 | csv_file (string): Path to the csv file with annotations.
31 | root_dir (string): Directory with all the images.
32 | transform (callable, optional): Optional transform to be applied
33 | on a sample.
34 | """
35 | self.metadata = pd.read_json(metadata_json, lines=True)
36 | self.image_dir = image_dir
37 | self.transform = transform
38 |
39 | def __len__(self):
40 | return len(self.metadata)
41 |
42 | def __getitem__(self, idx):
43 | if torch.is_tensor(idx):
44 | idx = idx.tolist()
45 | metadata = self.metadata.iloc[idx]
46 | with open(os.path.join(self.image_dir, metadata["id"] + ".jpg"), "rb") as f:
47 | image = Image.open(f).convert("RGB")
48 | return self.transform(image), metadata["id"]
49 |
50 |
51 | data_transform = transforms.Compose([
52 | transforms.Resize((224, 224)),
53 | transforms.ToTensor(),
54 | transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
55 | ])
56 | dataset = ArtDataset(metadata_fn, data_dir, data_transform)
57 | data_loader = torch.utils.data.DataLoader(dataset, batch_size=64, shuffle=False, num_workers=4)
58 |
59 | dataset_size = len(dataset)
60 |
61 | # Get a batch of training data
62 | model = models.resnet50(pretrained=True)
63 | model.eval()
64 | model.to(device)
65 | cut_model = nn.Sequential(*list(model.children())[:-1])
66 |
67 | all_outputs = []
68 | all_ids = []
69 | for i, (inputs, ids) in enumerate(tqdm(data_loader)):
70 | inputs = inputs.to(device)
71 | outputs = torch.squeeze(cut_model(inputs)).detach().cpu().numpy()
72 | all_outputs.append(outputs)
73 | all_ids.append(list(ids))
74 |
75 | all_outputs = np.concatenate(all_outputs, axis=0)
76 | all_ids = np.concatenate(all_ids, axis=0)
77 |
78 | with open(features_file, "wb+") as f:
79 | pickle.dump((all_outputs, all_ids), f)
80 |
81 | with open(features_file, "rb") as f:
82 | with torch.no_grad():
83 | (all_outputs, all_ids) = pickle.load(f)
84 | all_urls = np.array(pd.read_json(metadata_fn, lines=True).loc[:, "Thumbnail_Url"])
85 | features = torch.from_numpy(all_outputs).float().to("cpu:0")
86 | features = features / torch.sqrt(torch.sum(features ** 2, dim=1, keepdim=True))
87 | features = features.to(device)
88 | indicies = torch.arange(0, features.shape[0]).to(device)
89 | print("loaded features")
90 |
91 | metadata = pd.read_json(metadata_fn, lines=True)
92 | culture_arr = np.array(metadata["Culture"])
93 | cultures = metadata.groupby("Culture").count()["id"].sort_values(ascending=False).index.to_list()
94 | media_arr = np.array(metadata["Classification"])
95 | media = metadata.groupby("Classification").count()["id"].sort_values(ascending=False).index.to_list()
96 | ids = np.array(metadata["id"])
97 |
98 |
99 | masks = {"culture": {}, "medium": {}}
100 | for culture in cultures:
101 | masks["culture"][culture] = torch.from_numpy(culture_arr == culture).to(device)
102 | for medium in media:
103 | masks["medium"][medium] = torch.from_numpy(media_arr == medium).to(device)
104 |
105 |
106 | all_matches = []
107 | for i, row in tqdm(metadata.iterrows()):
108 | feature = features[i]
109 | matches = {"culture": {}, "medium": {}}
110 | all_dists = torch.sum(features * feature, dim=1).to(device)
111 | for culture in cultures:
112 | selected_indicies = indicies[masks["culture"][culture]]
113 | k = min(10, selected_indicies.shape[0])
114 | dists, inds = torch.topk(all_dists[selected_indicies], k, sorted=True)
115 | matches["culture"][culture] = ids[selected_indicies[inds].cpu().numpy()]
116 | for medium in media:
117 | selected_indicies = indicies[masks["medium"][medium]]
118 | k = min(10, selected_indicies.shape[0])
119 | dists, inds = torch.topk(all_dists[selected_indicies], k, sorted=True)
120 | matches["medium"][medium] = ids[selected_indicies[inds].cpu().numpy()]
121 | all_matches.append(matches)
122 |
123 | metadata["matches"] = all_matches
124 |
125 | metadata.to_json("results/metadata_enriched.json")
126 | print("here")
127 |
128 |
129 |
--------------------------------------------------------------------------------
/developer_guide.md:
--------------------------------------------------------------------------------
1 |
2 | # Mosaic Developer Guide
3 |
4 | ## Building from Scratch
5 |
6 | ### Backend
7 |
8 | To deploy your own conditional image retrieval search index please follow these steps
9 |
10 | 1. Download Image Metadata:
11 | ```bash
12 | wget https://mmlsparkdemo.blob.core.windows.net/cknn/metadata.json?sv=2019-02-02&st=2020-07-23T02%3A22%3A30Z&se=2023-07-24T02%3A22%3A00Z&sr=b&sp=r&sig=hDnGw9y%2BO5XlggL6br%2FPzSKmpAdUZ%2F1LJKVkcmbVmCE%3D
13 | ```
14 | 1. Download Images:
15 | ```bash
16 | cd data_prep
17 | python download_images.py
18 | ```
19 | 1. Featurize and perform Conditional Image Retrieval on every image
20 | ```bash
21 | cd data_prep
22 | python featurize_and_match.py
23 | ```
24 | 1. Write enriched information to an Azure Search Index.
25 |
26 | More detailed code coming soon, Follow [the closely related guide](
27 | https://docs.microsoft.com/en-us/azure/cognitive-services/big-data/recipes/art-explorer) for a similiar example.
28 |
29 | ### Frontend
30 |
31 | To build a copy of the mosaic website locally please use the following. Note that the introductory animations were made and deployed using a different framework.
32 |
33 | #### Development
34 |
35 | 1. Install `npm` if you dont already have it. You can find instructions at [https://nodejs.org/](https://nodejs.org/).
36 | 1. Install dependencies:
37 | ```bash
38 | cd frontend
39 | npm install
40 | ```
41 | 1. Start the development server:
42 | ```bash
43 | npm start
44 | ```
45 | 1. Navigate to [http://localhost:3000/art](http://localhost:3000/art) to explore the local website.
46 |
47 | #### Deployment
48 |
49 | 1. run `npm run build` to create a optimized build
50 | 1. Copy `frontend/utils/404.html` file to the `frontend/build` directory
51 | 1. In `frontend/build/index.html`, between the noscript and script tags (right after the root div), copy in the script from `frontend/utils/singe_page_app.txt`
52 | 1. run `npm run deploy` to push the build to github pages
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 | .env
3 |
4 | # dependencies
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/frontend/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation. All rights reserved.
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 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Installing and Running
2 | To run this, you'll need the latest version of Node.js installed.
3 | Open the project root and run `npm install`.
4 |
5 | To access the Rijksmuseum API, create a `.env` file in the project root with the following text:
6 |
7 | ```
8 | REACT_APP_RIJKSMUSEUM_API_KEY={yourapikey}
9 | ```
10 |
11 | where `{yourapikey}` is replaced by your personal API key. More details about the API are found [here](https://data.rijksmuseum.nl/object-metadata/api/).
12 |
13 | Then run `npm start`.
14 |
15 | # Design framework
16 | This project uses React Fabric UI.
17 |
18 | https://developer.microsoft.com/en-us/fabric#/controls/web/contextualmenu
19 |
20 | # Prepareing for Build
21 | Since we use Client Side routing, some hacks need to be introduced to get the site working on github pages.
22 |
23 | Step One: Modify Source Code
24 |
25 | 1. Package.json
26 | After the "name" line insert the line
27 | "homepage":"https://microsoft.github.io/art/",
28 |
29 | This enables the website to know what its base URL is.
30 |
31 | 2. ResultArtwork.tsx: exploreArtUrlSuffix(), line 42
32 | change '/' to '/art/'
33 |
34 | 3. NavBar.tsx, line 23
35 | change '/' to '/art/'
36 |
37 | 4. ResultBox.tsx: exploreArtUrlSuffic(), line 36
38 | change '/' to '/art/'
39 |
40 | NOTE: It is not needed to change the URL generation in SubmitControl, since it "pushes" too the end of the homepage URL.
41 |
42 | Step Two: Make the Build
43 |
44 | run 'npm run build'
45 | This makes a build and populates the build folder.
46 |
47 |
48 |
49 | Step Three: Hack the Build
50 | We follow the instrucions on this page: https://github.com/rafrex/spa-github-pages
51 |
52 | 1. In the build folder, create a 404.html file, and copy in the contents from the above link.
53 | On line 26, change segmentCount from 0 to 1. This enables the site to handle the /art/ at the end of the homepage URL.
54 |
55 | 2. In the html, between the noscript and script tags (right after the root div), copy in the script from the above link
56 |
57 |
58 | Step Four: Deploy the Hacked Build
59 |
60 | run 'npm run deploy'
61 | This takes the contents of the build folder, and deploys it to the github pages. It also makes a push to the gh-pages branch of a repo.
62 |
63 |
64 | # Converting From Deploy to Local
65 |
66 | Revert the changes made in the Source Code in Step One of Preparing for Build
67 |
68 |
69 | # Todo
70 |
71 | Add Information at the bottom of the page
72 | - This would be accomplished by adding to the end of the ExplorePage Heirarchy
73 |
74 | Carousel Random Picks
75 | - This involves changing the default queries made in ExplorePage
76 |
77 | Move Matches Functionality
78 | - This would involve moving the call stack found in an ExplorePage Button to wherever else the functionality needs to be
79 |
80 | MultiAcces Queries
81 | - This would probably involve a change of the APi and a change how we handle the data from the Dropdown menus
82 |
83 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "art",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.9.0",
7 | "@microsoft/applicationinsights-react-js": "^2.3.1",
8 | "@microsoft/applicationinsights-web": "^2.3.1",
9 | "@reach/router": "^1.2.1",
10 | "@types/reach__router": "^1.2.6",
11 | "@types/react-burger-menu": "^2.6.1",
12 | "@types/react-helmet": "^5.0.15",
13 | "@types/react-lazyload": "^2.6.0",
14 | "@types/react-router": "^5.1.4",
15 | "@types/react-share": "^3.0.3",
16 | "@types/react-slick": "^0.23.4",
17 | "@uifabric/fluent-theme": "^0.16.7",
18 | "@uifabric/react-cards": "^0.109.3",
19 | "@uifabric/react-hooks": "^7.0.1",
20 | "base64url": "^3.0.1",
21 | "gh-pages": "^2.2.0",
22 | "jimp": "^0.9.3",
23 | "jpeg-js": ">=0.4.0",
24 | "node-sass": "^4.14.1",
25 | "office-ui-fabric-react": "^6.174.0",
26 | "pure-react-carousel": "^1.24.1",
27 | "react": "^16.8.6",
28 | "react-burger-menu": "^2.7.0",
29 | "react-dom": "^16.8.6",
30 | "react-helmet": "^5.2.1",
31 | "react-i18next": "^11.2.7",
32 | "react-iframe": "^1.8.0",
33 | "react-lazyload": "^2.6.5",
34 | "react-responsive-carousel": "^3.1.51",
35 | "react-router-dom": "^5.1.2",
36 | "react-scripts": "^3.4.3",
37 | "react-share": "^3.0.1",
38 | "react-slick": "^0.25.2",
39 | "react-transition-group": "^4.3.0",
40 | "react-with-breakpoints": "^4.0.4",
41 | "reactjs-popup": "^1.5.0",
42 | "slick-carousel": "^1.8.1",
43 | "typescript": "3.4.5"
44 | },
45 | "scripts": {
46 | "deploy": "gh-pages -d build",
47 | "start": "react-scripts start",
48 | "build": "react-scripts build",
49 | "test": "react-scripts test",
50 | "eject": "react-scripts eject"
51 | },
52 | "eslintConfig": {
53 | "extends": "react-app"
54 | },
55 | "browserslist": {
56 | "production": [
57 | ">0.2%",
58 | "not dead",
59 | "not op_mini all"
60 | ],
61 | "development": [
62 | "last 1 chrome version",
63 | "last 1 firefox version",
64 | "last 1 safari version"
65 | ]
66 | },
67 | "resolutions": {
68 | "@babel/runtime": "7.0.0-beta.46"
69 | },
70 | "devDependencies": {
71 | "@types/jest": "^24.0.12",
72 | "@types/node": "^11.13.8",
73 | "@types/react": "^16.8.15",
74 | "@types/react-dom": "^16.9.4",
75 | "@types/react-router-dom": "^5.1.3",
76 | "typescript": "^3.4.5"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | Mosaic: Find Hidden Artistic Connections with Deep Learning
23 |
24 |
25 |
26 |
29 |
30 |
31 | You need to enable JavaScript to run this app.
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "mosAIc",
3 | "name": "mosAIc",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/public/mosaicTabLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/frontend/public/mosaicTabLogo.png
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | pointer-events: none;
9 | }
10 |
11 | .App-header {
12 | background-color: #282c34;
13 | min-height: 100vh;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | justify-content: center;
18 | font-size: calc(10px + 2vmin);
19 | color: white;
20 | }
21 |
22 | .App-link {
23 | color: #61dafb;
24 | }
25 |
26 | @keyframes App-logo-spin {
27 | from {
28 | transform: rotate(0deg);
29 | }
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/frontend/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render( , div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | // import './main.scss';
2 | import { initializeIcons} from 'office-ui-fabric-react';
3 | import React from 'react';
4 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
5 | import withAppInsights from './Shared/AppInsights';
6 | import ExplorePage from './ExplorePage/ExplorePage';
7 | import SearchPage from "./SearchPage/SearchPage";
8 | import IntroPage from "./IntroPage/IntroPage";
9 | import FairnessPage from "./IntroPage/FairnessPage";
10 |
11 | initializeIcons();
12 |
13 | class App extends React.Component {
14 | render() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | } />
22 | } />
23 | } />
24 |
25 |
26 | );
27 | }
28 | }
29 |
30 | export default withAppInsights(App);
--------------------------------------------------------------------------------
/frontend/src/ExplorePage/DefaultArtwork.tsx:
--------------------------------------------------------------------------------
1 | export const defaultArtworks = [
2 |
3 | {
4 | "id": "NjQ0Mg==",
5 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/6442.jpg",
6 | "Title": "Plate",
7 | "defaultCulture": "russian",
8 | "defaultMedium": "photographs",
9 | },
10 | {
11 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/rijks/resized_images/SK-A-3064.jpg",
12 | "id": "U0stQS0zMDY0",
13 | "Title": "Portrait of a Girl Dressed in Blue",
14 | "defaultCulture": "japanese",
15 | "defaultMedium": "precious"
16 | },
17 | {
18 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/545088.jpg",
19 | "Title": "Scarab",
20 | "id": "NTQ1MDg4",
21 | "defaultCulture": "greek",
22 | "defaultMedium": "paper"
23 | },
24 | {
25 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/363552.jpg",
26 | "Title": "Illuminated Letter D within a Decorated Border",
27 | "id": "MzYzNTUy",
28 | "defaultCulture": "chinese",
29 | "defaultMedium": "woodwork"
30 | },
31 | {
32 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/rijks/resized_images/BK-1978-878.jpg",
33 | "Title": "Double Face Banyan",
34 | "id": "QkstMTk3OC04Nzg=",
35 | "defaultCulture": "ancient_american",
36 | "defaultMedium": "musical_instruments"
37 | },
38 | {
39 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/10158.jpg",
40 | "Title": "Sunrise on the Matterhorn",
41 | "id": "MTAxNTg=",
42 | "defaultCulture": "greek",
43 | "defaultMedium": "paper"
44 | },
45 | {
46 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/437685.jpg",
47 | "Title": "The Road from Versailles to Louveciennes",
48 | "id": "NDM3Njg1",
49 | "defaultCulture": "russian",
50 | "defaultMedium": "prints"
51 | },
52 | {
53 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/rijks/resized_images/BK-16394-R-2.jpg",
54 | "Title": "Saucer with a putto on clouds",
55 | "id": "QkstMTYzOTQtUi0y",
56 | "defaultCulture": "ancient_european",
57 | "defaultMedium": "accessories"
58 | },
59 | {
60 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/205288.jpg",
61 | "Title": "Tureen with cover in the form of a turkey",
62 | "id": "MjA1Mjg4",
63 | "defaultCulture": "chinese",
64 | "defaultMedium": "stone"
65 | },
66 | {
67 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/249477.jpg",
68 | "Title": "Tile mosaic with rabbit, lizard and mushroom",
69 | "id": "MjQ5NDc3",
70 | "defaultCulture": "american",
71 | "defaultMedium": "drawings"
72 |
73 | },
74 | {
75 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/rijks/resized_images/BK-1973-268.jpg",
76 | "Title": "Kokarde van veren in de kleuren oranje en blauw in de vorm van een rozet",
77 | "id": "QkstMTk3My0yNjg=",
78 | "defaultCulture": "south_asian",
79 | "defaultMedium": "drawings"
80 | },
81 | {
82 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/rijks/resized_images/RP-T-1941-89.jpg",
83 | "Title": "Portrait of Edwin vom Rath’s Pug",
84 | "id": "UlAtVC0xOTQxLTg5",
85 | "defaultCulture": "african",
86 | "defaultMedium": "metalwork"
87 | },
88 | {
89 | "id": "UlAtVC0xOTE0LTE4LTM5",
90 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/rijks/resized_images/RP-T-1914-18-39.jpg",
91 | "Title": "Lachenalia aloides (L.f.) Engl. var. aurea (Opal flower)",
92 | "defaultCulture": "german",
93 | "defaultMedium": "precious"
94 | },
95 | {
96 | "id": "NTQ0MjEx",
97 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/544211.jpg",
98 | "Title": "Model Paddling Boat",
99 | "defaultCulture": "swiss",
100 | "defaultMedium": "photographs"
101 | },
102 | {
103 | "id": "OTQ4Mw==",
104 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/9483.jpg",
105 | "Title": "Vase",
106 | "defaultCulture": "austrian",
107 | "defaultMedium": "textiles"
108 | },
109 | {
110 | "id": "NTQ0NTA5",
111 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/544509.jpg",
112 | "Title": "Menat necklace from Malqata",
113 | "defaultCulture": "southeast_asian",
114 | "defaultMedium": "prints"
115 | },
116 |
117 | {
118 | "id": "MjM2NzUw",
119 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/236750.jpg",
120 | "Title": "Wine cooler with A Marine Triumph of Bacchus",
121 | "defaultCulture": "greek",
122 | "defaultMedium": "woodwork",
123 | "defaultResultId": "MjA3Njgx"
124 | },
125 | {
126 | "id": "MjAyNTU0",
127 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/202554.jpg",
128 | "Title": "Miniature tea and coffee set",
129 | "defaultCulture": "dutch",
130 | "defaultMedium": "drawings"
131 | },
132 | {
133 | "id": "MjU0MDI=",
134 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/25402.jpg",
135 | "Title": "Saddle",
136 | "defaultCulture": "american",
137 | "defaultMedium": "stone",
138 | "defaultResultId": "NDQ5ODEx"
139 | },
140 |
141 | {
142 | "id": "UlAtVC0xOTE0LTE3LTMxNg==",
143 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/rijks/resized_images/RP-T-1914-17-316.jpg",
144 | "Title": "Balearica regulorum (Grey crowned crane)",
145 | "defaultCulture": "southeast_asian",
146 | "defaultMedium": "ceramics"
147 | },
148 | {
149 | "id": "MjY1Mjk0",
150 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/265294.jpg",
151 | "Title": "[The Dolly Sisters]",
152 | "defaultCulture": "latin_american",
153 | "defaultMedium": "precious"
154 | },
155 | {
156 | "id": "MjM4OTI0",
157 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/238924.jpg",
158 | "Title": "Tile with dragon",
159 | "defaultCulture": "german",
160 | "defaultMedium": "sculptures"
161 | },
162 | {
163 | "id": "MjAzMjEw",
164 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/203210.jpg",
165 | "Title": "Standing woman",
166 | "defaultMedium": "musical_instruments",
167 | "defaultCulture": "ancient_american",
168 | },
169 | {
170 | "id": "MjcxNzM=",
171 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/27173.jpg",
172 | "Title": "Helmet (Khula Khud) with Horns",
173 | "defaultCulture": "german",
174 | "defaultMedium": "photographs"
175 |
176 | },
177 | {
178 | "id": "MTU3MTU5",
179 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/157159.jpg",
180 | "Title": "Umbrella",
181 | "defaultCulture": "african",
182 | "defaultMedium": "metalwork"
183 |
184 | },
185 | {
186 | "id": "MTk0MTEz",
187 | "Thumbnail_Url": "https://mmlsparkdemo.blob.core.windows.net/met/thumbnails/194113.jpg",
188 | "Title": "The Great Ruby Watch",
189 | "defaultCulture": "chinese",
190 | "defaultMedium": "textiles",
191 | "defaultResultId": "QkstQlItNjA5",
192 | },
193 |
194 |
195 |
196 | ]
197 |
198 | interface StringMap {
199 | [key: string]: any;
200 | }
201 |
202 | export const idToArtwork: StringMap = defaultArtworks.reduce((o, artwork) => Object.assign(o, { [artwork.id]: artwork }), {});
203 |
--------------------------------------------------------------------------------
/frontend/src/ExplorePage/ListCarousel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Slider from 'react-slick';
3 | import { ArtObject, ArtMatch } from '../Shared/ArtSchemas';
4 | import CircularProgress from '@material-ui/core/CircularProgress';
5 | import { Image } from 'office-ui-fabric-react';
6 | import LazyLoad from 'react-lazyload';
7 |
8 | interface IProps {
9 | items: ArtMatch[],
10 | selectorCallback: (item: any) => void,
11 | selectedArtwork: ArtObject | null
12 | }
13 |
14 | interface IState {
15 |
16 | }
17 |
18 | class ListCarousel extends React.Component {
19 |
20 | createGrid(): JSX.Element[] {
21 | let grid: JSX.Element[] = [];
22 | this.props.items.forEach((item: any, i: number) => {
23 |
24 | let isSelected = false
25 | if (this.props.selectedArtwork !== null && item.id === this.props.selectedArtwork!.id) {
26 | isSelected = true
27 | }
28 | grid.push(
29 | this.props.selectorCallback(item)}>
30 | }>
35 |
36 |
37 |
38 | );
39 | })
40 |
41 | return grid;
42 | }
43 |
44 | render(): JSX.Element {
45 | const settings = {
46 | centerMode: true,
47 | infinite: true,
48 | centerPadding: '60px',
49 | slidesToShow: 5,
50 | speed: 500,
51 | swipeToSlide: true,
52 | adaptiveHeight: true,
53 | focusOnSelect: true,
54 | slidesToScroll: 1,
55 | responsive: [
56 | {
57 | breakpoint: 1650,
58 | settings: {
59 | slidesToShow: 4,
60 | infinite: true,
61 | }
62 | },
63 | {
64 | breakpoint: 1440,
65 | settings: {
66 | slidesToShow: 3,
67 | infinite: true
68 | }
69 | },
70 | {
71 | breakpoint: 980,
72 | settings: {
73 | slidesToShow: 2,
74 | initialSlide: 2
75 | }
76 | },
77 | {
78 | breakpoint: 640,
79 | settings: {
80 | slidesToShow: 1,
81 | }
82 | }
83 | ]
84 | };
85 |
86 | return (
87 |
88 | {this.createGrid()}
89 |
90 | )
91 | }
92 | }
93 |
94 | export default ListCarousel;
--------------------------------------------------------------------------------
/frontend/src/ExplorePage/Options.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, makeStyles } from '@material-ui/core';
2 | import Select from '@material-ui/core/Select';
3 | import React from 'react';
4 | import { Text } from 'office-ui-fabric-react';
5 |
6 | const useSelectStyles = makeStyles({
7 | root: {
8 | width: "212px",
9 | margin: "auto",
10 | height: "25px",
11 | border: "2px solid black",
12 | textTransform: "capitalize",
13 | fontSize: "1rem",
14 | paddingLeft: "10px",
15 | outline: "none",
16 | fontWeight: "bold",
17 | fontFamily: "'Segoe UI', 'SegoeUI', -apple-system, BlinkMacSystemFont, 'Roboto', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
18 | },
19 | select: {
20 | outline: "none"
21 | }
22 | });
23 |
24 | interface IProps {
25 | value: string,
26 | changeConditional: any,
27 | choices: string[]
28 | }
29 |
30 | export default function Options(props: IProps) {
31 | const selectClasses = useSelectStyles();
32 |
33 | return (
34 |
35 | Closest
36 |
37 | { props.changeConditional(event.target.value) }}
41 | classes={{
42 | root: selectClasses.root
43 | }}>
44 | {props.choices.map((choice, index) => ({choice.replace(/_/g, " ")} ))}
45 |
46 |
47 | Artworks:
48 |
49 |
50 | );
51 | }
--------------------------------------------------------------------------------
/frontend/src/ExplorePage/OverlayMap.tsx:
--------------------------------------------------------------------------------
1 | import { ArtObject } from "../Shared/ArtSchemas";
2 | import base64url from "base64url";
3 |
4 | type stringMapping = {
5 | [key: string]: string
6 | }
7 |
8 | export const interpretationMapping: stringMapping = {
9 | 'SK-A-3064': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-paintings.jpg',
10 | 'SK-A-3905': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-french.jpg',
11 | 'SK-A-406': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-german.jpg',
12 | 'SK-A-4020': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-italian.jpg',
13 | 'SK-A-1048': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-british.jpg',
14 | 'SK-C-1183': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-belgian.jpg',
15 | 'RP-F-F01161-N': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-japanese.jpg',
16 | 'RP-T-2005-170': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-american.jpg',
17 | 'AK-RAK-2014-6': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-chinese.jpg',
18 | 'SK-A-3779': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-southeast_asian.jpg',
19 | 'RP-T-2016-11-2': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-european_general.jpg',
20 | 'RP-F-F01184-AR': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-roman.jpg',
21 | 'SK-A-4342': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-swiss.jpg',
22 | 'RP-P-1936-453': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-austrian.jpg',
23 | 'RP-P-1920-2701': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-czech.jpg',
24 | 'SK-A-2963': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-spanish.jpg',
25 | 'RP-T-1914-17-69': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-african.jpg',
26 | 'SK-A-4221': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-various.jpg',
27 | 'RP-F-00-7520': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-prints.jpg',
28 | 'RP-T-00-1180': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-drawings.jpg',
29 | 'BK-1975-35': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-ceramics.jpg',
30 | 'BK-1961-111': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-textiles.jpg',
31 | 'BK-14649-B': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-accessories.jpg',
32 | 'BK-C-1994-1': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-sculptures.jpg',
33 | 'SK-A-5049': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-glass.jpg',
34 | 'NG-2016-50-2': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-paper.jpg',
35 | 'AK-MAK-9': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-musical_instruments.jpg',
36 | 'SK-A-233': 'https://mmlsparkdemo.blob.core.windows.net/rijks/shap/demo2-uncategorized.jpg'
37 | };
38 |
39 | export function getInterpretation(artObject: ArtObject): string | null {
40 | if (artObject.id == null) {
41 | return null
42 | } else {
43 | return interpretationMapping[base64url.decode(artObject.id)]
44 | }
45 | }
--------------------------------------------------------------------------------
/frontend/src/ExplorePage/QueryArtwork.tsx:
--------------------------------------------------------------------------------
1 | import { Image, Stack, Text } from 'office-ui-fabric-react';
2 | import { DirectionalHint, TooltipDelay, TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
3 | import React from 'react';
4 | import { HideAt, ShowAt } from 'react-with-breakpoints';
5 | import { ArtObject } from '../Shared/ArtSchemas';
6 | import rijksImg from '../images/Rijks.jpg';
7 | import metImg from '../images/the_met_logo_crop.png';
8 |
9 | interface IState {
10 | objIDs: any,
11 | hover: boolean
12 | }
13 |
14 | type ArtworkProps = {
15 | artwork: ArtObject,
16 | }
17 |
18 | // Component for the original image that is used for the query (left image)
19 | export default class QueryArtwork extends React.Component {
20 |
21 | constructor(props: any) {
22 | super(props);
23 |
24 | this.state = {
25 | objIDs: [],
26 | hover: false
27 | };
28 | }
29 |
30 | jsonToURI(json: any) { return encodeURIComponent(JSON.stringify(json)); }
31 |
32 | render() {
33 | let musImg = (this.props.artwork.Museum === 'rijks') ? : ;
34 | let imgURL = this.props.artwork.Thumbnail_Url;
35 |
36 | return (
37 |
38 |
39 |
40 |
41 | {this.props.artwork.Title ? this.props.artwork.Title : "Untitled Piece"}
42 | {this.props.artwork.Culture.replace(/_/g, " ")}
43 | {this.props.artwork.Classification.replace(/_/g, " ")}
44 |
45 |
46 | this.setState({ hover: true })} onMouseLeave={() => this.setState({ hover: false })}>
47 |
48 |
55 |
56 | Query Image
57 |
58 |
59 |
60 |
61 |
62 | this.setState({ hover: true })} onMouseLeave={() => this.setState({ hover: false })}>
63 | this.setState({ hover: true })} onMouseLeave={() => this.setState({ hover: false })}>
64 |
65 |
72 |
73 | Query Image
74 |
75 |
76 |
77 |
78 | )
79 |
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/frontend/src/ExplorePage/ResultArtwork.tsx:
--------------------------------------------------------------------------------
1 | import { Image, Stack, Text } from 'office-ui-fabric-react';
2 | import { Shimmer, ShimmerElementType } from 'office-ui-fabric-react/lib/Shimmer';
3 | import { DirectionalHint, TooltipDelay, TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
4 | import React from 'react';
5 | import { HideAt, ShowAt } from 'react-with-breakpoints';
6 | import { ArtObject} from '../Shared/ArtSchemas';
7 | import rijksImg from '../images/Rijks.jpg';
8 | import metImg from '../images/the_met_logo_crop.png';
9 |
10 |
11 | interface IState {
12 | objIDs: any,
13 | hover: boolean
14 | }
15 |
16 | type ArtworkProps = {
17 | artwork: ArtObject,
18 | }
19 |
20 | // Component for the currently selected result image (right image)
21 | class ResultArtwork extends React.Component {
22 |
23 | constructor(props: any) {
24 | super(props);
25 |
26 | this.state = {
27 | objIDs: [],
28 | hover: false
29 | };
30 | }
31 |
32 | jsonToURI(json: any) { return encodeURIComponent(JSON.stringify(json)); }
33 |
34 |
35 | render() {
36 |
37 | var musImg
38 | if (this.props.artwork.Museum === 'rijks') {
39 | musImg =
40 | } else{
41 | musImg =
42 | };
43 |
44 | const imgURL = this.props.artwork.Thumbnail_Url;
45 | let dataLoaded = this.props.artwork.id == null ? false : true;
46 |
47 | return (
48 |
49 |
50 |
51 |
52 |
53 |
63 |
64 | {"Close Match"}
65 |
66 |
67 | {this.props.artwork.Title ? this.props.artwork.Title : "Untitled Piece"}
68 | {this.props.artwork.Culture.replace(/_/g, " ")}
69 | {this.props.artwork.Classification.replace(/_/g, " ")}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
94 | {"Close Match"}
95 |
96 |
97 |
98 |
99 | )
100 |
101 | }
102 | };
103 |
104 | export default ResultArtwork;
--------------------------------------------------------------------------------
/frontend/src/ExplorePage/SubmitControl.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Redirect } from 'react-router-dom';
3 |
4 | interface IProps {
5 | placeholder: string
6 | };
7 |
8 | interface IState {
9 | value: any,
10 | shouldRedirect: any,
11 | searchLink: any
12 | }
13 | /**
14 | * The Search Bar at the top of the Explore Page
15 | */
16 | export default class SubmitControl extends Component {
17 | constructor(props: any) {
18 | super(props);
19 | this.state = {
20 | value: '',
21 | shouldRedirect: false,
22 | searchLink: ''
23 | };
24 | }
25 |
26 | onChange = (event: any) => {
27 | this.setState({ value: event.target.value });
28 | };
29 |
30 | /**
31 | * OnSubmit event to instruct the component to redirect
32 | * to the Search Page with the generated URL
33 | */
34 | onSubmit = (event: any) => {
35 | event.preventDefault();
36 | let searchLink = this.getSearchUrl(this.state.value)
37 | this.setState({ shouldRedirect: true, searchLink: searchLink });
38 | }
39 |
40 | /**
41 | * Generates the search page url with the search query parameter
42 | * @param searchString The string in the search bar to be used to make the search query
43 | */
44 | getSearchUrl(searchString: string) {
45 | let urlBase = '/search/';
46 |
47 | let queryURL = '?query=' + searchString;
48 | let url = encodeURIComponent(queryURL);
49 | return urlBase + url;
50 | }
51 |
52 | render() {
53 | const { value } = this.state.value;
54 | if (this.state.shouldRedirect) {
55 | //Redirects to the search page using the url generated in getSearchUrl()
56 | return ;
57 |
58 | } else {
59 | return (
60 |
69 |
70 | )
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/frontend/src/IntroPage/FairnessPage.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Stack } from 'office-ui-fabric-react';
3 | import NavBar from '../Shared/NavBar';
4 | import { logEvent } from '../Shared/AppInsights';
5 |
6 | interface IProps {
7 | history: any
8 | };
9 |
10 | export default class FairnessPage extends React.Component {
11 | constructor(props: any) {
12 | super(props);
13 | }
14 |
15 |
16 | render() {
17 | return (
18 |
19 |
20 |
21 |
Before we get started:
22 |
23 |
24 |
25 | Art, culture, and heritage are sensitive subjects that require respect.
26 | This work aims to celebrate culture and diversity and explore art in a new way.
27 | AI has known biases and the results or artworks within this tool do not reflect
28 | the views of Microsoft, MIT, or the authors. We ask users to be respectfull of other's cultures and to use this tool responsibly.
29 | Some artworks or matches might not be approporiate for all ages, or might be culturally inappropriate.
30 | We have released this tool as-is, and encourage users to read our
transparency note for more info. Thanks!
31 |
32 |
33 |
34 | {
35 | this.props.history.push("/app");
36 | logEvent("Agree", {});
37 |
38 | }}>I Understand
39 |
40 |
41 |
42 |
43 |
44 | )
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/src/IntroPage/IntroPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Iframe from 'react-iframe'
3 |
4 | export default class IntroPage extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
16 |
17 |
18 | )
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/SearchPage/ResultBox.tsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from '@material-ui/core/CircularProgress';
2 | import { Card } from '@uifabric/react-cards';
3 | import { Image, ImageFit, Stack } from 'office-ui-fabric-react';
4 | import React, { Component } from 'react';
5 | import LazyLoad from 'react-lazyload';
6 | import { CSSTransition } from 'react-transition-group';
7 | import { urlEncodeArt } from '../Shared/ArtSchemas';
8 | import Popup from 'reactjs-popup'
9 | import { isBeta, betaMessageDiv } from '../Shared/BetaTools';
10 |
11 |
12 | interface IProps {
13 | key: any,
14 | data: any,
15 | handleTrackEvent: (eventName: string, properties: Object) => void
16 | }
17 |
18 | interface IState {
19 | hover: boolean
20 | open: boolean
21 | }
22 |
23 | /**
24 | * One box in the Search Grid
25 | */
26 | export default class ResultBox extends Component {
27 | constructor(props: any) {
28 | super(props);
29 | this.state = {
30 | hover: false,
31 | open: false
32 | };
33 |
34 | this.openModal = this.openModal.bind(this);
35 | this.closeModal = this.closeModal.bind(this);
36 | };
37 |
38 | openModal() {
39 | this.setState({ open: true });
40 | }
41 | closeModal() {
42 | this.setState({ open: false });
43 | }
44 |
45 | render() {
46 | let museumName = this.props.data.Museum === "met" ? "The Met" : "The Rijks";
47 |
48 | return (
49 |
50 | this.setState({ hover: true })} onMouseLeave={() => this.setState({ hover: false })}>
51 |
52 | {
53 | if (isBeta) {
54 | window.location.href = urlEncodeArt(this.props.data.id);
55 | } else {
56 | this.openModal()
57 | };
58 | }}>
59 | }
64 | >
65 |
66 |
67 |
68 |
73 |
74 |
75 | ×
76 |
77 | {betaMessageDiv}
78 |
79 |
80 |
81 |
82 |
83 | {!this.props.data.Title ?
84 | "Untitled Piece" :
85 | this.props.data.Title.length < 55 ? this.props.data.Title : this.props.data.Title.substring(0, 55) + "..."}
86 |
87 |
88 | {!this.props.data.Artist ? "No known artist" : this.props.data.Artist}
89 |
90 |
91 |
92 |
93 | this.props.handleTrackEvent("Source", { "Location": "SearchPage", "ArtLink": this.props.data.Link_Resource })}
96 | className="grid-card__button_link"
97 | target="_blank"
98 | rel="noopener noreferrer">View Source at {museumName}
99 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 | }
--------------------------------------------------------------------------------
/frontend/src/SearchPage/SearchControl.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | interface IProps{
4 | updateTerms: any
5 | };
6 |
7 | interface IState {
8 | value: any
9 | }
10 | /**
11 | * Search Text input box across the top of the search page
12 | * updatesTerms prop: Callback to send up the search bar text
13 | */
14 | export default class SearchControl extends Component {
15 | constructor(props:any) {
16 | super(props);
17 | this.state = {
18 | value: '', //The text value in the search bar
19 | };
20 | }
21 |
22 | onChange = (event:any) => {
23 | this.setState({ value: event.target.value });
24 | this.props.updateTerms([event.target.value]);
25 | };
26 |
27 | render() {
28 | const { value } = this.state.value;
29 | return ;
30 | }
31 | }
--------------------------------------------------------------------------------
/frontend/src/SearchPage/SearchGrid.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Stack } from 'office-ui-fabric-react';
3 | import ResultBox from './ResultBox';
4 |
5 | interface IProps {
6 | results: any,
7 | handleTrackEvent: (eventName: string, properties: Object) => void
8 | };
9 |
10 | /**
11 | * Grid used to display results of a search
12 | * 'results' prop: an array of the json results from the Azure search (the 'value' value)
13 | */
14 | export default class SearchGrid extends Component {
15 | render() {
16 | return (
17 |
18 | {this.props.results.map((result: any) => (
19 |
20 | ))}
21 |
22 | );
23 | }
24 | }
--------------------------------------------------------------------------------
/frontend/src/SearchPage/SearchPage.tsx:
--------------------------------------------------------------------------------
1 | import { Separator, Stack } from 'office-ui-fabric-react';
2 | import React from 'react';
3 | import { appInsights } from '../Shared/AppInsights';
4 | import SearchControl from './SearchControl';
5 | import SearchGrid from './SearchGrid';
6 | import TagList from './TagList';
7 | import { search} from '../Shared/SearchTools';
8 | import NavBar from '../Shared/NavBar';
9 |
10 | interface IProps {
11 | match: any
12 | };
13 |
14 | interface IState {
15 | terms: string[], // Current search query terms to be displayed
16 | activeFilters: { [key: string]: Set }, // Active tags used for filtering the search results
17 | facets: { [key: string]: string[] }, // Available filtering tags for the current search terms (currently top 5 most common tags)
18 | results: object[] // Search results
19 | };
20 |
21 | const facetNames = ["Culture", "Museum", "Classification"];
22 |
23 |
24 | export class SearchPage extends React.Component {
25 |
26 | constructor(props: IProps) {
27 | super(props);
28 | this.state = {
29 | terms: ['*'], // Current search query to be displayed
30 | activeFilters: {},
31 | facets: {},
32 | results: []
33 | };
34 | this.updateTerms = this.updateTerms.bind(this);
35 | this.clearActiveFilters = this.clearActiveFilters.bind(this);
36 | this.selectAndApplyFilters = this.selectAndApplyFilters.bind(this);
37 | this.handleTrackEvent = this.handleTrackEvent.bind(this);
38 |
39 | //AppInsights.downloadAndSetup({ instrumentationKey: "7ca0d69b-9656-4f4f-821a-fb1d81338282" });
40 | //AppInsights.trackPageView("Search Page");
41 | }
42 |
43 | componentDidMount() {
44 | const searchTerm = this.props.match.params.id; // The Search term from the Explore Page
45 |
46 | if (searchTerm) {
47 | let decodedSearchTerm = decodeURIComponent(searchTerm);
48 |
49 | let queryString = decodedSearchTerm.split("&")[0].slice(7);
50 | this.setState({terms:[queryString]},() => this.executeSearch(true));
51 | } else {
52 | this.setState({ terms: ["*"] }, () => this.executeSearch(true))
53 | }
54 | }
55 |
56 |
57 |
58 | /**
59 | * This function creates a brand new search query request and refreshes all tags and results in the current state
60 | * @param updateFacets whether to retrieve new filter tags after the search (e.g. French, Sculptures)
61 | */
62 | executeSearch(updateFacets: boolean): void {
63 | let self = this
64 | search(self.state.terms, facetNames, self.state.activeFilters)
65 | .then(function (responseJson) {
66 | if (updateFacets) {
67 | self.setState({ facets: responseJson["@search.facets"], results: responseJson.value });
68 | }
69 | else {
70 | self.setState({ results: responseJson.value });
71 | }
72 |
73 | });
74 | }
75 |
76 | uriToJSON(urijson: any) { return JSON.parse(decodeURIComponent(urijson)); }
77 |
78 | updateTerms(newTerms: any) {
79 | this.setState({ terms: newTerms}, () => this.executeSearch(true));
80 | }
81 |
82 | clearActiveFilters() {
83 | this.setState({ activeFilters: {} }, () => this.executeSearch(true));
84 | }
85 |
86 | setUnion(a: any, b: any) {
87 | return new Set([...a, ...b])
88 | }
89 |
90 | /**
91 | * Handler for selecting and applying filters immediately
92 | * @param category the category of filter to update (e.g. Culture, Department)
93 | * @param value the specific filter to toggle (e.g. French, Sculputres)
94 | */
95 | selectAndApplyFilters(category: any, value: any) {
96 | let af = this.state.activeFilters;
97 |
98 | // Adds the new category to active filters if it does not exist
99 | if (!Object.keys(af).includes(category)) {
100 | af[category] = new Set();
101 | }
102 |
103 | // Toggles the active status of the filter
104 | if (af[category].has(value)) {
105 | af[category].delete(value);
106 | }
107 | else {
108 | af[category].add(value);
109 | }
110 |
111 | // Update the state and search with the new active filters
112 | this.setState({ activeFilters: af }, () => this.executeSearch(false));
113 | }
114 |
115 |
116 | /**
117 | * Handles event tracking for interactions
118 | * @param eventName Name of the event to send to appInsights
119 | * @param properties Custom properties to include in the event data
120 | */
121 | async handleTrackEvent(eventName: string, properties: Object) {
122 | appInsights.trackEvent({ name: eventName, properties: properties });
123 | }
124 |
125 | render() {
126 | return (
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | )
147 | }
148 | }
149 |
150 | export default SearchPage
--------------------------------------------------------------------------------
/frontend/src/SearchPage/TagList.tsx:
--------------------------------------------------------------------------------
1 | import { DefaultButton } from 'office-ui-fabric-react';
2 | import React, { ChangeEvent, Component } from 'react';
3 |
4 | interface IProps {
5 | activeFilters:any,
6 | facets:any,
7 | clearActiveFilters:any,
8 | selectAndApplyFilters:any
9 | }
10 |
11 |
12 | /**
13 | * List of tags that can be used to filter results
14 | */
15 | export default class TagList extends Component {
16 | constructor(props:any) {
17 | super(props);
18 | this.state = {};
19 | }
20 |
21 | /**
22 | * Handles filter updates related to a checkbox change
23 | * @param event the triggered checkbox event
24 | * @param category the category of the filter to update (e.g. Culture, Department)
25 | * @param value the specific filter to toggle (e.g. French, Sculptures)
26 | */
27 | onCheckboxChange(event:ChangeEvent, category: any, value: any) {
28 | this.props.selectAndApplyFilters(category, value);
29 | }
30 |
31 | /**
32 | * Returns if a filter is active based on state in the parent
33 | * @param activeFilters object containing the current active filters
34 | * @param name the category of the filter to check (e.g. Culture, Department)
35 | * @param value the specific filter to toggle (e.g. French, Sculptures)
36 | */
37 | isChecked(activeFilters:any, name:string, value:string) {
38 | return activeFilters[name] != null && activeFilters[name].has(value);
39 | }
40 |
41 | render() {
42 | return (
43 |
44 |
45 | {Object.entries(this.props.facets).map((nameFacetEntries:any,) =>
46 |
47 | {nameFacetEntries[0]}
48 | {nameFacetEntries[1].map((facetInfo:any) =>
49 |
50 | this.onCheckboxChange(e, nameFacetEntries[0], facetInfo.value)} />
55 | {facetInfo.value + ` (${facetInfo.count})`}
56 |
57 | )}
58 |
59 | )}
60 |
61 | );
62 | }
63 | }
--------------------------------------------------------------------------------
/frontend/src/Shared/AppInsights.tsx:
--------------------------------------------------------------------------------
1 | // AppInsights.js
2 | import { ApplicationInsights } from '@microsoft/applicationinsights-web'
3 | import { ReactPlugin, withAITracking } from '@microsoft/applicationinsights-react-js'
4 | import { globalHistory } from "@reach/router"
5 |
6 | const reactPlugin = new ReactPlugin();
7 | const ai = new ApplicationInsights({
8 | config: {
9 | instrumentationKey: "38354ab7-5e2c-4c70-956b-b9fcfc4506e8",
10 | extensions: [reactPlugin],
11 | extensionConfig: {
12 | [reactPlugin.identifier]: { history: globalHistory }
13 | }
14 | }
15 | })
16 | ai.loadAppInsights()
17 |
18 | export default (Component:any) => withAITracking(reactPlugin, Component)
19 | export const appInsights = ai.appInsights
20 |
21 | /**
22 | * Handles event tracking for interactions
23 | * @param eventName Name of the event to send to appInsights
24 | * @param properties Custom properties to include in the event data
25 | */
26 | export async function logEvent(eventName: string, properties: Object) {
27 | appInsights.trackEvent({ name: eventName, properties: properties });
28 | }
--------------------------------------------------------------------------------
/frontend/src/Shared/ArtSchemas.tsx:
--------------------------------------------------------------------------------
1 | import { root } from './Constants'
2 |
3 | /**
4 | * Internal representation of an artwork
5 | */
6 | export class ArtObject {
7 |
8 | Artist: string;
9 | Classification: string;
10 | Culture: string;
11 | Image_Url: string;
12 | Museum: string;
13 | Museum_Page: string;
14 | Thumbnail_Url: string;
15 | Title: string;
16 | id: string | null;
17 |
18 | constructor(
19 | Artist: string,
20 | Classification: string,
21 | Culture: string,
22 | Image_Url: string,
23 | Museum: string,
24 | Museum_Page: string,
25 | Thumbnail_Url: string,
26 | Title: string,
27 | id: string | null
28 | ) {
29 | this.Artist = Artist;
30 | this.Classification = Classification;
31 | this.Culture = Culture;
32 | this.Image_Url = Image_Url;
33 | this.Museum = Museum;
34 | this.Museum_Page = Museum_Page;
35 | this.Thumbnail_Url = Thumbnail_Url;
36 | this.Title = Title;
37 | this.id = id;
38 | }
39 | }
40 |
41 | export const loadingArtwork = new ArtObject("", "", "", "", "", "", "", "", null)
42 |
43 | export class ArtMatch {
44 | Thumbnail_Url: string;
45 | id: string | null;
46 | Title: string | null;
47 |
48 | constructor(
49 | Thumbnail_Url: string,
50 | id: string | null,
51 | Title: string | null
52 | ) {
53 | this.Thumbnail_Url = Thumbnail_Url;
54 | this.id = id;
55 | this.Title = Title
56 | }
57 | }
58 |
59 | export const loadingMatch = new ArtMatch("./images/loading.jpg", null, null)
60 |
61 | export function urlEncodeArt(artworkId: string) {
62 | return root + '/app/' + encodeURIComponent('?id=' + artworkId);
63 | }
--------------------------------------------------------------------------------
/frontend/src/Shared/BetaTools.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const isBeta = true
4 |
5 | export const betaMessageDiv =
6 | Thank you for exploring mosaic! To explore the full application please apply to access the beta
here
10 |
--------------------------------------------------------------------------------
/frontend/src/Shared/Constants.tsx:
--------------------------------------------------------------------------------
1 | export const root = "";
2 |
--------------------------------------------------------------------------------
/frontend/src/Shared/NavBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import logo from '../images/mosaicLogo.png';
3 | import { FacebookIcon, FacebookShareButton, LinkedinIcon, LinkedinShareButton, TwitterIcon, TwitterShareButton } from 'react-share';
4 | import { logEvent } from '../Shared/AppInsights';
5 | import { slide as Menu } from 'react-burger-menu'
6 | import { HideAt, ShowAt } from 'react-with-breakpoints';
7 | import { root } from './Constants'
8 |
9 | const shareUrl = "https://microsoft.github.io/art/";
10 |
11 | /**
12 | * NavBar across the top, the logo links to the Explore Page
13 | */
14 | export default class NavBar extends Component {
15 | render() {
16 | return (
17 |
18 |
23 |
24 |
25 |
logEvent("Share", { "Network": "Facebook" })}>
26 |
27 |
28 |
29 |
30 |
logEvent("Share", { "Network": "Twitter" })}>
31 |
32 |
33 |
34 |
35 |
logEvent("Share", { "Network": "Linkedin" })}>
36 |
37 |
38 |
39 |
40 |
41 | intro
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Intro
50 |
51 | Share:
52 | logEvent("Share", { "Network": "Facebook" })}>
53 |
54 |
55 |
56 |
57 | logEvent("Share", { "Network": "Twitter" })}>
58 |
59 |
60 |
61 |
62 | logEvent("Share", { "Network": "Linkedin" })}>
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | )
73 | };
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/frontend/src/Shared/SearchTools.tsx:
--------------------------------------------------------------------------------
1 | export const azureSearchUrl = 'https://extern-search.search.windows.net/indexes/art-search-3/docs';
2 | export const apiVersion = '2019-05-06'
3 | export const azureSearchApiKey = '0E8FACE23652EB8A6634F02B43D42E55';
4 | export const nMatches = 10;
5 | export const simpleFields: string[] = ["Artist", "Classification", "Culture", "Image_Url", "Museum", "Museum_Page", "Thumbnail_Url", "Title", "id"]
6 |
7 | // Options for filtering the art culture
8 | export const cultures: string[] = [
9 | 'african',
10 | 'american',
11 | 'ancient_american',
12 | 'ancient_asian',
13 | 'ancient_european',
14 | 'ancient_middle_eastern',
15 | 'asian',
16 | 'austrian',
17 | 'belgian',
18 | 'british',
19 | 'chinese',
20 | 'czech',
21 | 'dutch',
22 | 'egyptian',
23 | 'european',
24 | 'french',
25 | 'german',
26 | 'greek',
27 | 'iranian',
28 | 'italian',
29 | 'japanese',
30 | 'latin_american',
31 | 'middle_eastern',
32 | 'roman',
33 | 'russian',
34 | 'south_asian',
35 | 'southeast_asian',
36 | 'spanish',
37 | 'swiss',
38 | 'various'
39 | ];
40 |
41 | // Options for filtering the art medium
42 | export const media: string[] = [
43 | 'prints',
44 | 'drawings',
45 | 'ceramics',
46 | 'textiles',
47 | 'paintings',
48 | 'accessories',
49 | 'photographs',
50 | 'glass',
51 | 'metalwork',
52 | 'sculptures',
53 | 'weapons',
54 | 'stone',
55 | 'precious',
56 | 'paper',
57 | 'woodwork',
58 | 'leatherwork',
59 | 'musical_instruments',
60 | 'uncategorized'
61 | ];
62 |
63 |
64 | export function queryBase(query: string): Promise {
65 | return fetch(azureSearchUrl + query, { headers: { "Content-Type": "application/json", 'api-key': azureSearchApiKey } })
66 | .then(function (response) {
67 | return response.json();
68 | })
69 | }
70 |
71 | export function lookupBase(artworkID: string, selectors: string[]): Promise {
72 | return queryBase(`/${artworkID}?api-version=${apiVersion}&$select=${selectors.join(",")}`)
73 | }
74 |
75 | export function lookupWithMatches(artworkID: string, cultureFilter: string, mediumFilter: string): Promise {
76 | return lookupBase(artworkID, simpleFields.concat(["matches/culture/" + cultureFilter, "matches/medium/" + mediumFilter]))
77 | }
78 |
79 | export function lookup(artworkID: string): Promise {
80 | return lookupBase(artworkID, simpleFields)
81 | }
82 |
83 | export function search(
84 | terms: string[],
85 | facetNames: string[],
86 | activeFilters: { [key: string]: Set }
87 | ): Promise {
88 |
89 | function filterTerm(col: any, values: any) {
90 | return `search.in(${col}, '${[...values].join("|")}', '|')`
91 | }
92 |
93 | let query = "?api-version=" + apiVersion
94 | + "&search=" + terms.join('|')
95 | + "&$select=" + simpleFields.join(",")
96 | + facetNames.map(f => "&facet=" + f + "%2Ccount%3A8").join("")
97 |
98 | let filtersToSearch = Object.entries(activeFilters).filter((val: any) => val[1].size > 0)
99 |
100 | if (filtersToSearch.length !== 0) {
101 | query = query + "&$filter=" + filtersToSearch.map(([col, values],) =>
102 | filterTerm(col, values)
103 | ).join(" or ")
104 | }
105 |
106 | return queryBase(query)
107 | }
108 |
109 |
--------------------------------------------------------------------------------
/frontend/src/fonts/PlayfairDisplayBlack.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/frontend/src/fonts/PlayfairDisplayBlack.ttf
--------------------------------------------------------------------------------
/frontend/src/fonts/PlayfairDisplayRegular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/frontend/src/fonts/PlayfairDisplayRegular.ttf
--------------------------------------------------------------------------------
/frontend/src/images/Rijks.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/frontend/src/images/Rijks.jpg
--------------------------------------------------------------------------------
/frontend/src/images/ajax-loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/frontend/src/images/ajax-loader.gif
--------------------------------------------------------------------------------
/frontend/src/images/banner5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/frontend/src/images/banner5.jpg
--------------------------------------------------------------------------------
/frontend/src/images/icon-checkbox-outline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-checkbox.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-chevron-left-inverted.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-chevron-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-chevron-right-inverted.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-chevron-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-chevron.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-clear.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-earth.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-less.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-more.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/images/icon-search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/images/mosaicLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/frontend/src/images/mosaicLogo.png
--------------------------------------------------------------------------------
/frontend/src/images/the_met_logo_crop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/frontend/src/images/the_met_logo_crop.png
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import './main.scss';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import App from './App';
5 | import { FluentCustomizations } from '@uifabric/fluent-theme';
6 | import { Customizer, mergeStyles } from 'office-ui-fabric-react';
7 | import { BreakpointsProvider } from 'react-with-breakpoints';
8 | import * as serviceWorker from './serviceWorker';
9 |
10 | // Inject some global styles
11 | mergeStyles({
12 | selectors: {
13 | ':global(body), :global(html), :global(#root)': {
14 | margin: 0,
15 | padding: 0,
16 | height: '100vh'
17 | }
18 | }
19 | });
20 |
21 | ReactDOM.render(
22 |
23 |
24 |
25 |
26 | ,
27 | document.getElementById('root')
28 | );
29 |
30 | // If you want your app to work offline and load faster, you can change
31 | // unregister() to register() below. Note this comes with some pitfalls.
32 | // Learn more about service workers: https://bit.ly/CRA-PWA
33 | serviceWorker.unregister();
34 |
--------------------------------------------------------------------------------
/frontend/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/frontend/src/main.scss:
--------------------------------------------------------------------------------
1 | // 1. Configuration and helpers
2 | @import
3 | '/styles/abstracts/variables',
4 | // '/styles/abstracts/functions',
5 | // '/styles/abstracts/mixins/arrows',
6 | // '/styles/abstracts/mixins/aspect-ratio',
7 | // '/styles/abstracts/mixins/baseline',
8 | // '/styles/abstracts/mixins/custom-properties',
9 | // '/styles/abstracts/mixins/ellipsis',
10 | // '/styles/abstracts/mixins/ellipsis-multiline',
11 | // '/styles/abstracts/mixins/fonts',
12 | // '/styles/abstracts/mixins/crop-text',
13 | // '/styles/abstracts/mixins/underline',
14 | // '/styles/abstracts/mixins/spacing',
15 | '/styles/abstracts/mixins/container-centered',
16 | '/styles/abstracts/mixins/transition';
17 |
18 | // 2. Vendors
19 | @import
20 | '/styles/vendor/normalize',
21 | '/styles/vendor/slick';
22 |
23 | // 3. Base stuff
24 | @import
25 | // '/styles/base/base',
26 | // '/styles/base/fonts',
27 | // '/styles/base/typography',
28 | '/styles/base/utilities';
29 |
30 | // 4. Layout-related sections
31 | @import
32 | // '/styles/layout/header',
33 | '/styles/layout/selectpage-head';
34 | // '/styles/layout/footer';
35 |
36 | // 5. Components
37 | @import
38 | '/styles/components/btn',
39 | '/styles/components/explore',
40 | '/styles/components/search',
41 | '/styles/components/grid-card',
42 | // '/styles/components/original',
43 | '/styles/components/nav',
44 | '/styles/components/burger';
45 | // '/styles/components/gen-art',
46 | // '/styles/components/artwork-info';
47 |
48 | // 6. Page-specific styles
49 | @import
50 | // '/styles/pages/map',
51 | // '/styles/pages/explore';
52 | '/styles/pages/about';
53 |
54 |
--------------------------------------------------------------------------------
/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then(response => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type');
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then(registration => {
121 | registration.unregister().then(() => {
122 | window.location.reload();
123 | });
124 | });
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config);
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.'
133 | );
134 | });
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then(registration => {
140 | registration.unregister();
141 | });
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/_functions.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all application-wide Sass functions.
3 | // -----------------------------------------------------------------------------
4 |
5 | // Native `url(..)` function wrapper
6 | // @param {String} $base - base URL for the asset
7 | // @param {String} $type - asset type folder (e.g. `fonts/`)
8 | // @param {String} $path - asset path
9 | // @return {Url}
10 | @function asset($base, $type, $path) {
11 | @return url($base+$type+$path);
12 | }
13 |
14 | // Returns URL to an image based on its path
15 | // @param {String} $path - image path
16 | // @param {String} $base [$base-url] - base URL
17 | // @return {Url}
18 | // @require $base-url
19 | @function image($path, $base: $base-url) {
20 | @return asset($base, 'images/', $path);
21 | }
22 |
23 | // Returns URL to a font based on its path
24 | // @param {String} $path - font path
25 | // @param {String} $base [$base-url] - base URL
26 | // @return {Url}
27 | // @require $base-url
28 | @function font($path, $base: $base-url) {
29 | @return asset($base, 'fonts/', $path);
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/_variables.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all application-wide Sass variables.
3 | // -----------------------------------------------------------------------------
4 |
5 | //
6 | // #Colors
7 | //
8 |
9 | // #Base-Color
10 | $color-white: #ffffff;
11 | $color-base: #000000;
12 | $color-grey: #666666;
13 |
14 | $color-base-darkest: $color-base;
15 | $color-base-darker: lighten($color-base, 20);
16 | $color-base-dark: lighten($color-base, 40);
17 | $color-base-light: lighten($color-base, 60);
18 | $color-base-lighter: lighten($color-base, 80);
19 |
20 | // Primary-Colors
21 | $color-primary: #000000;
22 | $color-secondary: #000000;
23 | $color-secondary-hover: #030303;
24 |
25 | // #Greys
26 | $color-shade-dark: #e6e6e6;
27 | $color-shade-light: #f8f8f8;
28 |
29 | // #Text Colors
30 | $color-text-light: #ffffff;
31 |
32 | // #Slick Colors
33 | $color-slide-background: $color-shade-light;
34 | $color-arrow-background: $color-white;
35 | $color-slide-text: #ff0000;
36 |
37 | // #Primary-Color
38 |
39 | //
40 | // #Box-Shadow
41 | //
42 | $box-shadow: 0 1px 5px 0 rgba($color-primary, 0.3);
43 |
44 | //
45 | // #Animations
46 | //
47 | $animation-speed: 0.25s;
48 | $animation-type-in: ease-in;
49 | $animation-type-cubic: cubic-bezier(0, 0, 0.3, 1);
50 |
51 | //
52 | // #Fonts
53 | //
54 | $font-family-base: 'Segoe UI', 'SegoeUI', -apple-system, BlinkMacSystemFont, 'Roboto', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
55 | $font-family-heading: 'Playfair Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
56 |
57 | //
58 | // $Line-Height
59 | //
60 | $line-height: 1.4;
61 |
62 | //
63 | // $Font-Weight
64 | //
65 | $font-weight-thin: 100;
66 | $font-weight-extralight: 200;
67 | $font-weight-light: 300;
68 | $font-weight-normal: 400;
69 | $font-weight-medium: 500;
70 | $font-weight-semibold: 600;
71 | $font-weight-bold: 700;
72 | $font-weight-extrabold: 800;
73 | $font-weight-black: 900;
74 |
75 | //
76 | // #Scale
77 | //
78 | $scale: 1.25;
79 |
80 | //
81 | // #Font-Size
82 | //
83 | $font-s: 0.75rem; // 12px
84 | $font-m: 1rem; // 16px
85 | $font-l: 1.125rem; // 18px
86 |
87 | //
88 | // #Spacing
89 | //
90 | $container-width: 50em; // 800px
91 | $container-padding: 1.25em; // 20px
92 | $space-default: $font-m; // Is dependant on $font-m and should be 1em;
93 |
94 | //
95 | // #Space Sizes
96 | //
97 | $space-xxs: $font-m / 8; // 2px
98 | $space-xs: $font-m / 4; // 4px
99 | $space-s: $font-m / 2; // 8px
100 | $space-m: $font-m; // 16px
101 | $space-l: $font-m * 2; // 32px
102 | $space-xl: $font-m * 4; // 64px
103 | $space-xxl: $font-m * 8; // 128px
104 |
105 | //
106 | // #Border-Radius Sizes
107 | //
108 | $border-radius-xxs: $space-xxs; // 2px
109 | $border-radius-xs: $space-xs; // 4px
110 | $border-radius-s: $space-s; // 8px
111 | $border-radius-m: $space-m; // 16px
112 | $border-radius-l: $space-l; // 32px
113 | $border-radius-xl: $space-xl; // 64px
114 | $border-radius-xxl: $space-xxl; // 128px
115 |
116 | // #Icon Sizes
117 | $icon-xxs: $space-xxs; // 2px
118 | $icon-xs: $space-xs; // 4px
119 | $icon-s: $space-s; // 8px
120 | $icon-m: $space-m; // 16px
121 | $icon-l: $space-l; // 32px
122 | $icon-xl: $space-xl; // 64px
123 | $icon-xxl: $space-xxl; // 128px
124 |
125 | //
126 | // #Media-Queries
127 | //
128 | $mq-xxs: 20em; // 320px
129 | $mq-xs: 30em; // 480px
130 | $mq-ss: 40em; //640px
131 | $mq-s: 48em; // 768px
132 | $mq-m: 61.25em; // 980px
133 | $mq-l: 80em; // 1280px
134 | $mq-xl: 90em; // 1440px
135 | $mq-xxl: 160em; // 2650px
136 |
137 | //
138 | // $Z-Index
139 | //
140 | $z-index-s: 1;
141 | $z-index-m: 2;
142 | $z-index-l: 3;
143 | $z-index-xl: 4;
144 |
145 | //
146 | // Custom Sizes
147 | //
148 | $button-size: 2.5rem;
149 | $container-width: 67.5rem; // 1080
150 | $gen-size: 16.5rem; // 264px
151 | $horizontal-space: 6.25rem;
152 | $horizontal-foo-padding: 2.875rem;
153 | $letter-spacing: 3px;
154 | $map-description-width: 34rem; // 544px
155 | $map-padding: 3rem;
156 | $nav-unit: 4rem;
157 | $nav-unit-s: 3rem;
158 |
159 | $original-img-size: 6rem;
160 | $tags-size: 12.5rem;
161 |
162 | //
163 | // Slick carousel custom Sizes
164 | //
165 | $arrow-size: 5rem;
166 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_arrows.scss:
--------------------------------------------------------------------------------
1 | @mixin box-arrow($arrowDirection, $arrowColor, $arrowSize: 10px) {
2 | position: relative;
3 | z-index: 10;
4 |
5 | &::after {
6 | content: '';
7 | width: 0;
8 | height: 0;
9 | display: block;
10 | position: absolute;
11 | z-index: 10;
12 | border: 0;
13 |
14 | @if $arrowDirection == bottom or $arrowDirection == top {
15 | border-left: $arrowSize solid transparent;
16 | border-right: $arrowSize solid transparent;
17 | margin-left: -$arrowSize;
18 | left: 50%;
19 |
20 | @if $arrowDirection == bottom {
21 | border-top: $arrowSize solid $arrowColor;
22 | bottom: -$arrowSize;
23 | }
24 |
25 | @if $arrowDirection == top {
26 | border-bottom: $arrowSize solid $arrowColor;
27 | top: -$arrowSize;
28 | }
29 | }
30 |
31 | @if $arrowDirection == left or $arrowDirection == right {
32 | border-top: $arrowSize solid transparent;
33 | border-bottom: $arrowSize solid transparent;
34 | margin-top: -$arrowSize;
35 | top: 50%;
36 |
37 | @if $arrowDirection == left {
38 | border-right: $arrowSize solid $arrowColor;
39 | left: -$arrowSize;
40 | }
41 |
42 | @if $arrowDirection == right {
43 | border-left: $arrowSize solid $arrowColor;
44 | left: auto;
45 | right: -$arrowSize;
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_aspect-ratio.scss:
--------------------------------------------------------------------------------
1 | // Fixed Aspect Ratio
2 | // @params {String} $property, $variable, $fallback
3 |
4 | @mixin aspect-ratio($width, $height) {
5 | position: relative;
6 | &:before {
7 | display: block;
8 | content: '';
9 | width: 100%;
10 | padding-top: ($height / $width) * 100%;
11 | }
12 | > .inner-box {
13 | position: absolute;
14 | top: 0;
15 | left: 0;
16 | right: 0;
17 | bottom: 0;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_baseline.scss:
--------------------------------------------------------------------------------
1 | $base-font-size: 16px;
2 | $base-line-height: $line-height;
3 |
4 | // this value may vary for each font
5 | // unitless value relative to 1em
6 | $cap-height: 0.68;
7 |
8 | @mixin baseline($font-size, $scale: 2) {
9 | // rhythm unit
10 | $rhythm: $base-line-height * $font-size / $scale;
11 |
12 | // number of rhythm units that can fit the font-size
13 | $lines: ceil(($font-size + 0.001px) / $rhythm);
14 |
15 | // calculate the new line-height
16 | $line-height: $rhythm * $lines / $font-size;
17 |
18 | // use the results
19 | font-size: $font-size;
20 | line-height: $line-height;
21 |
22 | $baseline-distance: ($line-height - $cap-height) / 2;
23 |
24 | // METHOD 1
25 |
26 | // this method can relatively move down elements you may not want to
27 | // position: relative;
28 | // top: $baseline-distance + em;
29 |
30 | // METHOD 2
31 |
32 | // if you use this mixin only on elements that have one direction margins
33 | // http://csswizardry.com/2012/06/single-direction-margin-declarations/
34 | // you can use this method with no worries.
35 | // this method assumes the direction is down and the margin is $base-line-height
36 | padding-top: $baseline-distance + em;
37 | margin-bottom: $base-line-height - $baseline-distance + em;
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_container-centered.scss:
--------------------------------------------------------------------------------
1 | // -------------------------------------------------------------
2 | // Centered container with a max-width of 1080px
3 | // -------------------------------------------------------------
4 | @mixin container-centered() {
5 | margin-left: auto;
6 | margin-right: auto;
7 | max-width: $container-width;
8 | width: 100%;
9 | }
10 |
11 | @mixin main-content_responsive {
12 | @media (max-width: $mq-l){
13 | max-width: 80%;
14 | }
15 |
16 | @media (max-width: $mq-m){
17 | max-width: 90%;
18 | }
19 |
20 | @media (max-width: $mq-s){
21 | max-width: 95%;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_crop-text.scss:
--------------------------------------------------------------------------------
1 | // Mixin generated at: http://text-crop.eightshapes.com/?
2 |
3 | // Usage Examples
4 | // .my-level-1-heading-class {
5 | // @include text-crop; // Will use default line height of 1.3
6 | // font-size: 48px;
7 | // margin: 0 0 0 16px;
8 | // }
9 | //
10 | // .my-level-2-heading-class {
11 | // @include text-crop; // Will use default line height of 1.3
12 | // font-size: 32px; // Don't need to change any settings, will work with any font size automatically
13 | // margin: 0 0 0 16px;
14 | // }
15 | //
16 | // .my-body-copy-class {
17 | // @include text-crop($line-height: 2); // Larger line height desired, set the line height via the mixin
18 | // font-size: 16px;
19 | // }
20 | //
21 | // // Sometimes depending on the font-size, the rendering, the browser, etc. you may need to tweak the output.
22 | // // You can adjust the top and bottom cropping when invoking the component using the $top-adjustment and $bottom-adjustment settings
23 | //
24 | // .slight-adjustment-needed {
25 | // @include text-crop($top-adjustment: -0.5px, $bottom-adjustment: 2px);
26 | // font-size: 17px;
27 | // }
28 | //
29 | // .dont-do-this {
30 | // @include text-crop;
31 | // font-size: 16px;
32 | // line-height: 3; // DO NOT set line height outside of the mixin, the mixin needs the line height value to calculate the crop correctly
33 | // }
34 | //
35 |
36 | @mixin text-crop($line-height: 1.3, $top-adjustment: 0px, $bottom-adjustment: 0px) {
37 | // Configured in Step 1
38 | $top-crop: 10;
39 | $bottom-crop: 8;
40 | $crop-font-size: 36;
41 | $crop-line-height: 1.2;
42 |
43 | // Apply values to calculate em-based margins that work with any font size
44 | $dynamic-top-crop: max(
45 | ($top-crop + ($line-height - $crop-line-height) * ($crop-font-size / 2)),
46 | 0
47 | ) / $crop-font-size;
48 | $dynamic-bottom-crop: max(
49 | ($bottom-crop + ($line-height - $crop-line-height) * ($crop-font-size / 2)),
50 | 0
51 | ) / $crop-font-size;
52 |
53 | // Mixin output
54 | line-height: $line-height;
55 |
56 | &::before,
57 | &::after {
58 | content: '';
59 | display: block;
60 | height: 0;
61 | width: 0;
62 | }
63 |
64 | &::before {
65 | margin-bottom: calc(-#{$dynamic-top-crop}em + #{$top-adjustment});
66 | }
67 |
68 | &::after {
69 | margin-top: calc(-#{$dynamic-bottom-crop}em + #{$bottom-adjustment});
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_custom-properties.scss:
--------------------------------------------------------------------------------
1 | // Custom properties to Sass fallback
2 | // @author Ignacio Villanueva
3 | // @params {String}: $customProp, $renderedValue
4 | // Source: https://gist.github.com/IgnaciodeNuevo/4a945cec3c68bb6a0ad829bd14c96918
5 |
6 | @mixin props($customProp, $renderedValue) {
7 | #{$customProp}: map-get($VariablesMap, $renderedValue);
8 | #{$customProp}: var(unquote('--#{$renderedValue}'));
9 | }
10 |
11 | // Usage:
12 | // .class {
13 | // @include props('CSS Property ', 'Sass Map Key');
14 | // }
15 |
16 | @mixin plainProps($property, $variable, $fallback) {
17 | #{$property}: $fallback;
18 | #{$property}: var($variable);
19 | }
20 |
21 | // Usage:
22 | // .dashboard {
23 | // @include variable(background, --theme-primary-color, blue);
24 | // }
25 |
26 | // Compiles to:
27 | // .dashboard {
28 | // background: blue;
29 | // background: var(--theme-primary-color);
30 | // }
31 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_ellipsis-multiline.scss:
--------------------------------------------------------------------------------
1 | // https://codepen.io/sergeysemashko/pen/jFGJD
2 | // $max-height - max-height property value. required
3 | // parameter to limit text in non-webkit browser.
4 | // Be careful with overriding properties.
5 | // Strongly recommended to include this mixin after all properties
6 | // to prevent overriding of display property:
7 | // .my block {
8 | // display: block;
9 | // ...
10 | // @include ellipsis(2em, 2);
11 | // }
12 | // Example of usage:
13 | // @include ellipsis(2em, 2);
14 |
15 | @mixin ellipsis($max-height, $lines: 2) {
16 | // Fallback for non-webkit browsers.
17 | // Fallback does not render ellipsis.
18 | overflow: hidden;
19 | max-height: $max-height;
20 |
21 | // Webkit solution for multiline ellipsis
22 | display: -webkit-box;
23 | -webkit-box-orient: vertical;
24 | -webkit-line-clamp: $lines;
25 |
26 | // Solution for Opera
27 | text-overflow: -o-ellipsis-lastline;
28 | }
29 |
30 | // example of ellipsis mixin usage
31 | .multiline-ellipsis {
32 | width: 30%;
33 | @include ellipsis(4rem, 4);
34 | }
35 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_ellipsis.scss:
--------------------------------------------------------------------------------
1 | // Add ellipsis at the end of a line to avoid text overflow
2 | // @author Ignacio Villanueva
3 | // @param {String} $width
4 |
5 | @mixin ellipsis($width: 100%) {
6 | max-width: $width;
7 | overflow: hidden;
8 | text-overflow: ellipsis;
9 | white-space: nowrap;
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_fonts.scss:
--------------------------------------------------------------------------------
1 | // @Author https://gist.github.com/jonathantneal/d0460e5c2d5d7f9bc5e6
2 |
3 | // =============================================================================
4 | // String Replace
5 | // =============================================================================
6 |
7 | @function str-replace($string, $search, $replace: '') {
8 | $index: str-index($string, $search);
9 |
10 | @if $index {
11 | @return str-slice($string, 1, $index - 1) + $replace +
12 | str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
13 | }
14 |
15 | @return $string;
16 | }
17 |
18 | // =============================================================================
19 | // Font Face
20 | // =============================================================================
21 |
22 | @mixin font-face($name, $path, $weight: null, $style: null, $exts: eot woff ttf svg) {
23 | $src: null;
24 | $extmods: (
25 | eot: '?',
26 | svg: '#' + str-replace($name, ' ', '_'),
27 | );
28 | $formats: (
29 | otf: 'opentype',
30 | ttf: 'truetype',
31 | );
32 |
33 | @each $ext in $exts {
34 | $extmod: if(map-has-key($extmods, $ext), $ext + map-get($extmods, $ext), $ext);
35 |
36 | $format: if(map-has-key($formats, $ext), map-get($formats, $ext), $ext);
37 |
38 | $src: append($src, url(quote($path + '.' + $extmod)) format(quote($format)), comma);
39 | }
40 |
41 | @font-face {
42 | font-family: quote($name);
43 | font-style: $style;
44 | font-weight: $weight;
45 | src: $src;
46 | }
47 | }
48 |
49 | @mixin font-include($font, $type, $weight: null, $style: null) {
50 | @include font-face(
51 | $font,
52 | '/assets/fonts/' + $font + '/' + $font + '-' + $type,
53 | $weight,
54 | $style
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_modular-scale.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains the Modular-Scale placeholders using Major Third ratio (1.25)
3 | // -----------------------------------------------------------------------------
4 | // https://www.modularscale.com/?1&em&1.25
5 |
6 | // Remember to install modularscale-sass: npm install modularscale-sass --save-dev
7 | @import '../../../node_modules/modularscale-sass/stylesheets/modularscale';
8 |
9 | $modularscale: (
10 | base: $font-m,
11 | ratio: $scale,
12 | );
13 |
14 | @mixin heading-h1() {
15 | font-size: ms(9);
16 | margin-top: 0;
17 | }
18 |
19 | @mixin heading-h2() {
20 | font-size: ms(5);
21 | margin-top: 0;
22 | }
23 |
24 | @mixin heading-h3() {
25 | font-size: ms(4);
26 | margin-top: 0;
27 | }
28 |
29 | @mixin heading-h4() {
30 | font-size: ms(3);
31 | margin-top: 0;
32 | }
33 |
34 | @mixin heading-h5() {
35 | font-size: ms(2);
36 | margin-top: 0;
37 | }
38 |
39 | @mixin heading-h6() {
40 | font-size: ms(1);
41 | margin-top: 0;
42 | }
43 |
44 | @mixin body-text() {
45 | font-size: ms(0);
46 | margin-top: 0;
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_spacing.scss:
--------------------------------------------------------------------------------
1 | // Spacings Mixin and Fallback
2 | // @params {String} $type, $property
3 | @mixin spacing($type: 's', $property: 'margin-top') {
4 | #{$property}: map-get($spacings, $type);
5 | #{$property}: var(--margin-#{$type});
6 | }
7 |
8 | // Usage
9 | // .p {
10 | // @include spacing(l, margin-bottom);
11 | // }
12 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_transition.scss:
--------------------------------------------------------------------------------
1 | // Transition Mixin
2 | // @params {String} $prop
3 | @mixin transition($prop-1: all, $prop-2: null, $prop-3: null) {
4 | transition-duration: $animation-speed;
5 | transition-property: $prop-1, $prop-2, $prop-3;
6 | transition-timing-function: $animation-type-cubic;
7 | }
8 |
9 | // Usage
10 | // .p {
11 | // @include transition(transform);
12 | // }
13 |
--------------------------------------------------------------------------------
/frontend/src/styles/abstracts/mixins/_underline.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains a text-decoration-skip: ink facke Sass mixin.
3 | // See: https://caniuse.com/#feat=text-decoration
4 | // See also: https://css-tricks.com/almanac/properties/t/text-decoration-skip/
5 | // -----------------------------------------------------------------------------
6 |
7 | @mixin underline($shadow-color: #fff, $border-color: currentColor, $border-width: 1px) {
8 | background-position: center bottom 30%;
9 | background-size: 100% $border-width;
10 | background: linear-gradient($border-color, $border-color) no-repeat;
11 | padding-bottom: 0.5em;
12 | padding-left: 0;
13 | padding-right: 0;
14 | padding-top: 0.5em;
15 | text-decoration: none;
16 | text-shadow:
17 | 3px 0 $shadow-color,
18 | 2px 0 $shadow-color,
19 | 1px 0 $shadow-color,
20 | -1px 0 $shadow-color,
21 | -2px 0 $shadow-color,
22 | -3px 0 $shadow-color;
23 | transition: color 0.1s ease-out;
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/src/styles/base/_base.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains very basic styles.
3 | // -----------------------------------------------------------------------------
4 |
5 | :root {
6 | box-sizing: border-box;
7 | font-size: 100%;
8 | line-height: $line-height;
9 | }
10 |
11 | //
12 | // Make all elements from the DOM inherit from the parent box-sizing
13 | // Since `*` has a specificity of 0, it does not override the `html` value
14 | // making all elements inheriting from the root box-sizing value
15 | // See: https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/
16 | //
17 | *,
18 | *::before,
19 | *::after {
20 | box-sizing: inherit;
21 | }
22 |
23 | // Resets and basic body styles
24 | body {
25 | margin: 0;
26 | padding: 0;
27 | min-height: 100%;
28 | min-width: 100%;
29 | }
30 |
31 | .main {
32 | flex: 1;
33 | max-width: 100%;
34 | margin-left: auto;
35 | margin-right: auto;
36 | z-index: 0;
37 | }
38 |
39 | .claim {
40 | color: $color-primary;
41 | font-size: 2.625rem;
42 | font-family: $font-family-heading;
43 | font-weight: $font-weight-light;
44 | line-height: initial;
45 | margin-bottom: $space-m;
46 | margin-top: 0;
47 | position: relative;
48 | width: 400px;
49 |
50 | // &:before {
51 | // background-color: $color-secondary;
52 | // content: '';
53 | // height: 2px;
54 | // position: absolute;
55 | // top: calc(#{$space-s} * -1);
56 | // width: 48px;
57 | // }
58 | }
59 |
--------------------------------------------------------------------------------
/frontend/src/styles/base/_fonts.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all @font-face declarations, if any.
3 | // -----------------------------------------------------------------------------
4 | @font-face {
5 | font-family: 'SegoeUI';
6 | src: url('//c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.woff2') format('woff2'),
7 | url('//c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.woff') format('woff'),
8 | url('//c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.ttf') format('ttf');
9 | font-weight: $font-weight-normal;
10 | }
11 |
12 | @font-face {
13 | font-family: 'SegoeUI';
14 | src: url('//c.s-microsoft.com/static/fonts/segoe-ui/west-european/Semibold/latest.woff2') format('woff2'),
15 | url('//c.s-microsoft.com/static/fonts/segoe-ui/west-european/Semibold/latest.woff') format('woff'),
16 | url('//c.s-microsoft.com/static/fonts/segoe-ui/west-european/Semibold/latest.ttf') format('ttf');
17 | font-weight: $font-weight-semibold;
18 | }
19 |
20 | @font-face {
21 | font-family: 'SegoeUI';
22 | src: url('//c.s-microsoft.com/static/fonts/segoe-ui/west-european/Bold/latest.woff2') format('woff2'),
23 | url('//c.s-microsoft.com/static/fonts/segoe-ui/west-european/Bold/latest.woff') format('woff'),
24 | url('//c.s-microsoft.com/static/fonts/segoe-ui/west-european/Bold/latest.ttf') format('ttf');
25 | font-weight: $font-weight-bold;
26 | }
27 |
28 | @font-face {
29 | font-family: 'Playfair Display';
30 | src: url('./fonts/PlayfairDisplayRegular.ttf') format('truetype');
31 | font-weight: $font-weight-normal;
32 | }
33 |
34 | @font-face {
35 | font-family: 'Playfair Display';
36 | src: url('./fonts/PlayfairDisplayBlack.ttf') format('truetype');
37 | font-weight: $font-weight-black;
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/styles/base/_typography.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Basic typography style for copy text
3 | //
4 |
5 | body {
6 | color: $color-primary;
7 | font-family: $font-family-base;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/styles/base/_utilities.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains CSS utiltiy classes.
3 | // -----------------------------------------------------------------------------
4 |
5 | @import '../abstracts/variables.scss';
6 |
7 | //
8 | // Clear inner floats
9 | //
10 | .u-clearfix::after {
11 | clear: both;
12 | content: '';
13 | display: table;
14 | }
15 |
16 | //
17 | // Main content containers
18 | // 1. Make the container full-width with a maximum width
19 | // 2. Center it in the viewport
20 | // 3. Leave some space on the edges, especially valuable on small screens
21 | //
22 | .u-container {
23 | max-width: $container-width; // 1
24 | margin-left: auto; // 2
25 | margin-right: auto; // 2
26 | padding-left: $container-padding; // 3
27 | padding-right: $container-padding; // 3
28 | width: 100%; // 1
29 | }
30 |
31 | //
32 | // Hide text while making it readable for screen readers
33 | // 1. Needed in WebKit-based browsers because of an implementation bug;
34 | // See: https://code.google.com/p/chromium/issues/detail?id=457146
35 | //
36 | .u-hide-text {
37 | overflow: hidden;
38 | padding: 0; // 1
39 | text-indent: 101%;
40 | white-space: nowrap;
41 | }
42 |
43 | //
44 | // Hide element while making it readable for screen readers
45 | // Shamelessly borrowed from HTML5Boilerplate:
46 | // https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css#L119-L133
47 | //
48 | .u-visually-hidden {
49 | border: 0;
50 | clip: rect(0 0 0 0);
51 | height: 1px;
52 | margin: -1px;
53 | overflow: hidden;
54 | padding: 0;
55 | position: absolute;
56 | width: 1px;
57 | }
58 |
59 | //
60 | // Dont break white-space
61 | //
62 | .u-nowrap {
63 | white-space: nowrap;
64 | }
65 |
66 | //
67 | // Centers a container
68 | //
69 | .u-container-centered {
70 | padding-left: $space-m;
71 | padding-right: $space-m;
72 |
73 | @include container-centered();
74 | }
75 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/_artwork-info.scss:
--------------------------------------------------------------------------------
1 | .artwork-info {
2 | display: flex;
3 |
4 | &__image {
5 | flex: 1;
6 | width: 5%;
7 | height: 5%;
8 | }
9 |
10 | &__detail {
11 | flex: 1
12 | }
13 | }
--------------------------------------------------------------------------------
/frontend/src/styles/components/_btn.scss:
--------------------------------------------------------------------------------
1 | // -------------------------------------------------------------
2 | // This file contains all styles related to the .button component
3 | // -------------------------------------------------------------
4 | .button {
5 | background-color: transparent;
6 | border: 2px solid $color-secondary;
7 | border-radius: initial;
8 | color: $color-secondary;
9 | font-size: $font-l;
10 | font-weight: $font-weight-bold;
11 | font-family: $font-family-base;
12 | letter-spacing: $letter-spacing;
13 | text-decoration: none;
14 | margin-left: 0;
15 | margin-right: 0;
16 | padding: $space-s;
17 | text-transform: uppercase;
18 | transition-duration: $animation-speed;
19 | transition-property: color, background-color;
20 | transition-timing-function: $animation-type-cubic;
21 | vertical-align: middle;
22 | will-change: box-shadow;
23 |
24 | &:hover {
25 | background-color: $color-base-dark;
26 | color: $color-text-light;
27 | border-color: $color-grey;
28 | cursor: pointer;
29 | }
30 |
31 | &:active {
32 | box-shadow: initial;
33 | }
34 |
35 | &__actions {
36 | margin-top: .5em;
37 | width: 100%;
38 | }
39 | }
40 |
41 | .button2 {
42 | background-color: transparent;
43 | border: 2px solid $color-secondary;
44 | border-radius: initial;
45 | color: $color-secondary;
46 | font-size: $font-m;
47 | font-weight: $font-weight-bold;
48 | letter-spacing: $letter-spacing;
49 | text-decoration: none;
50 | padding: $space-s;
51 | text-transform: uppercase;
52 | transition-duration: $animation-speed;
53 | transition-property: color, background-color;
54 | transition-timing-function: $animation-type-cubic;
55 | vertical-align: middle;
56 | will-change: box-shadow;
57 |
58 | &:hover {
59 | background-color: $color-base-dark;
60 | color: $color-text-light;
61 | border-color: $color-grey;
62 | cursor: pointer;
63 | }
64 |
65 | &:active {
66 | box-shadow: initial;
67 | }
68 |
69 | @media (max-width: $mq-m){
70 | font-size: $font-s;
71 | margin-bottom: 0;
72 | }
73 |
74 | &__actions {
75 | margin-top: .5em;
76 | width: 100%;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/_burger.scss:
--------------------------------------------------------------------------------
1 | /* Position and sizing of burger button */
2 | .bm-burger-button {
3 | position: fixed;
4 | width: 2rem;
5 | height: 2rem;
6 | right: 8px;
7 | top: 7px;
8 | }
9 |
10 | /* Color/shape of burger icon bars */
11 | .bm-burger-bars {
12 | background:black;
13 | }
14 |
15 | /* Color/shape of burger icon bars on hover*/
16 | .bm-burger-bars-hover {
17 | background: black;
18 | }
19 |
20 | /* Position and sizing of clickable cross button */
21 | .bm-cross-button {
22 | height: 36px !important;
23 | width: 36px !important;
24 | }
25 |
26 | /* Color/shape of close button cross */
27 | .bm-cross {
28 | background: #bdc3c7;
29 | }
30 |
31 | /*
32 | Sidebar wrapper styles
33 | Note: Beware of modifying this element as it can break the animations - you should not need to touch it in most cases
34 | */
35 | .bm-menu-wrap {
36 | position: fixed;
37 | height: 100%;
38 | width: 50% !important;
39 | margin-top: -24px;
40 | }
41 |
42 | /* General sidebar styles */
43 | .bm-menu {
44 | background: #373a47;
45 | padding: 2em 1em 0;
46 | font-size: 1.15em;
47 | }
48 |
49 | /* Morph shape necessary with bubble or elastic */
50 | .bm-morph-shape {
51 | fill: #373a47;
52 | }
53 |
54 | /* Wrapper for item list */
55 | .bm-item-list {
56 | color: #b8b7ad;
57 | padding: 0.8em;
58 | }
59 |
60 | /* Individual item */
61 | .bm-item {
62 | display: inline-block;
63 | }
64 |
65 | /* Styling of overlay */
66 | .bm-overlay {
67 | background: rgba(0, 0, 0, 0.3);
68 | }
--------------------------------------------------------------------------------
/frontend/src/styles/components/_explore.scss:
--------------------------------------------------------------------------------
1 | .explore {
2 | width: 100%;
3 |
4 | &__grid {
5 | width: 500px;
6 | }
7 |
8 | &__share-button {
9 | margin: 15px 10px;
10 | cursor: pointer;
11 | }
12 |
13 | &__buttons {
14 | width: 250px;
15 | margin-left: 10px;
16 | margin-right: 10px;
17 | margin-bottom: 10px;
18 |
19 | &:disabled {
20 | border-color: #D8D8D8;
21 | color: #D8D8D8;
22 | }
23 |
24 | &:hover:disabled {
25 | background-color: transparent;
26 | border-color: #D8D8D8;
27 | color: #D8D8D8;
28 | cursor: default;
29 | }
30 |
31 | }
32 |
33 | &__dropdown {
34 | justify-content: center;
35 | margin: 0px 15px !important;
36 | width: 200px !important;
37 |
38 | @media (max-width: $mq-l) {
39 | width: 175px !important;
40 | }
41 |
42 | @media (max-width: $mq-m) {
43 | width: 150px !important;
44 | }
45 |
46 | @media (max-width: $mq-s) {
47 | width: 200px !important;
48 | }
49 | }
50 |
51 | &__options-container {
52 | margin: auto;
53 | width: 200px;
54 | }
55 |
56 | &__main-images {
57 | padding: 10px;
58 |
59 | @media (max-width: $mq-l) {
60 | justify-content: center;
61 | }
62 | }
63 |
64 | &__card-img {
65 | height: 96%;
66 | max-width: 100%;
67 | margin: auto;
68 | object-fit: cover;
69 | overflow: visible !important;
70 |
71 | display: flex;
72 | flex: auto;
73 | align-items: center;
74 | justify-content: center;
75 | flex-direction: row;
76 | // box-shadow: 0px 0px 3px gray;
77 | max-width: 300px;
78 | z-index: calc(#{$z-index-s} * -1);
79 |
80 | @media (max-width: $mq-xl) {
81 | max-width: 275px;
82 | }
83 |
84 | @media (max-width: $mq-m) {
85 | max-width: 250px;
86 | }
87 |
88 | @media (max-width: $mq-ss) {
89 | max-width: 225px;
90 | }
91 | }
92 |
93 | &__card-img-container {
94 | height: calc(max(20vh, 200px));
95 | max-width: 100vw;
96 | outline: none;
97 | }
98 |
99 | &__grid-list-container {
100 | width: 100%;
101 |
102 | @media (max-width:1128px) {
103 | width: 100%;
104 | }
105 | }
106 |
107 | &__grid_old {
108 | display: grid;
109 | grid-template-columns: repeat(3, 1fr);
110 | grid-column-gap: 1.5rem;
111 | grid-row-gap: 5.5rem;
112 |
113 | @media (max-width: $mq-l){
114 | grid-template-columns: repeat(3, 1fr);
115 | }
116 |
117 | @media (max-width: $mq-m){
118 | grid-template-columns: repeat(2, 1fr);
119 | }
120 |
121 | @media (max-width: $mq-s){
122 | grid-template-columns: repeat(3, 1fr);
123 | }
124 |
125 | @media (max-width: $mq-xs){
126 | grid-template-columns: repeat(2, 1fr);
127 | }
128 |
129 | // Overrides PaletteBox.jsx styles
130 | .ldbqQH {
131 | margin: 0;
132 | height: auto;
133 | width: auto;
134 | z-index: 99;
135 | }
136 |
137 | // Overrides PaletteBox.jsx styles
138 | .iznKJF {
139 | margin-bottom: -70px;
140 | margin-left: 0;
141 | }
142 |
143 | // Overrides PaletteBox.jsx BUTTON styles
144 | .StyledButton-sc-323bzc-0 {
145 | align-items: center;
146 | background-color: $color-shade-light;
147 | border-radius: 50%;
148 | display: flex;
149 | justify-content: center;
150 | height: $button-size;
151 | width: $button-size;
152 | }
153 | }
154 |
155 | .dZUveh {
156 | padding: 0;
157 | }
158 |
159 |
160 | &__img-container {
161 | position: relative;
162 | overflow: hidden;
163 | margin-bottom: 10px;
164 | }
165 |
166 | &__slide-buttons {
167 | position: absolute;
168 | bottom: -30px;
169 | width: 100% !important;
170 | height: 30px;
171 | margin-top: auto !important;
172 | // box-shadow: 0px 5px 10px 5px gray;
173 | }
174 |
175 | &__slide-enter {
176 | bottom: -30px;
177 | }
178 |
179 | &__slide-enter-done {
180 | bottom: 0px;
181 | transition: bottom 0.15s linear;
182 | }
183 |
184 | &__slide-exit {
185 | bottom: 0px;
186 | }
187 |
188 | &__slide-exit-done {
189 | bottom: -30px;
190 | transition: bottom 0.25s linear;
191 | }
192 |
193 | &__slide-button-link {
194 | display: inline-block;
195 | text-align: center;
196 | text-decoration: none;
197 | width: 100%;
198 | color: black;
199 | // background-color: gainsboro;
200 | background-color: rgb(240,240,240);
201 | font-family: $font-family-base;
202 | font-weight: 500;
203 | font-size: $font-m;
204 | line-height: 30px;
205 | }
206 |
207 | &__slide-button-link:hover {
208 | background-color: gainsboro;
209 | }
210 |
211 | &__slide-button-sep {
212 | width: 1px;
213 | height: 30px;
214 | background-color: gainsboro;
215 | }
216 |
217 | &__museum-icon {
218 | position: absolute;
219 | top: 0;
220 | left: 0;
221 | opacity: 0.4;
222 |
223 | &:hover {
224 | opacity: 1;
225 | transition: opacity 0.4s;
226 | }
227 | }
228 |
229 | &__artwork-frame {
230 | position: relative;
231 | overflow: hidden;
232 | }
233 |
234 | &__background-banner {
235 | position: relative;
236 | width: 100%;
237 | height: 60vh;
238 | }
239 |
240 | &__background-banner-2 {
241 | position: relative;
242 | width: 100%;
243 | height: 30vh;
244 | }
245 |
246 |
247 | &__banner-text {
248 | position: absolute;
249 | width: 50%;
250 | left: 50%;
251 | transform: translate(-50%, 0%);
252 | bottom: 40%;
253 | color: white;
254 | text-align: center;
255 | font-family: $font-family-base;
256 | font-size: 36px;
257 | font-weight: bold;
258 | text-shadow: 0px 0px 40px black, 0px 0px 40px black, 0px 0px 40px black;
259 | }
260 |
261 | &__banner-text-2 {
262 | color: black;
263 | text-align: center;
264 | font-family: $font-family-base;
265 | font-size: 36px;
266 | font-weight: bold;
267 | }
268 |
269 | &__disclaimer-text {
270 | color: black;
271 | left: auto;
272 | @media (max-width: 1128px) {
273 | height: auto;
274 | margin-top: 5px !important;
275 | margin-left: 5% !important;
276 | margin-right: 5% !important;
277 | margin-bottom: 5% !important;
278 | font-size: 18px;
279 | }
280 | margin-top: 25px !important;
281 | margin-left: 10% !important;
282 | margin-right: 10% !important;
283 | margin-bottom: 10% !important;
284 |
285 | text-align: center;
286 | font-family: $font-family-base;
287 | font-size: 24px;
288 | font-weight: normal;
289 | }
290 |
291 | &__pick-image-text {
292 | color: black;
293 | left: auto;
294 | @media (max-width: 1128px) {
295 | height: auto;
296 | margin-top: 75px !important;
297 | }
298 | margin-top: 20px !important;
299 | text-align: center;
300 | font-family: $font-family-base;
301 | font-size: 36px;
302 | font-weight: bold;
303 | }
304 |
305 | &__big-text {
306 | color: black;
307 | left: auto;
308 | margin-top: 20px !important;
309 | text-align: center;
310 | font-family: $font-family-base;
311 | font-size: 36px;
312 | font-weight: bold;
313 | }
314 |
315 |
316 | &__medium-text {
317 | color: black;
318 | left: auto;
319 | margin-top: 20px !important;
320 | text-align: center;
321 | font-family: $font-family-base;
322 | font-size: 34px;
323 | @media (max-width: 1128px) {
324 | font-size: 18px;
325 | }
326 | padding: 20px
327 | }
328 |
329 | &__get-started {
330 | position: absolute;
331 | left: 50%;
332 | transform: translate(-50%, 0%);
333 | bottom: 10%;
334 | font-size: 28px;
335 | color: rgb(255, 255, 255);
336 | border: 2px solid white;
337 | background-color: #0000009e;
338 | }
339 |
340 | &__parallax {
341 | position: fixed;
342 | object-fit: cover;
343 | width: 100%;
344 | left: 50%;
345 | transform: translate(-50%, 0%);
346 | z-index: -1;
347 | }
348 |
349 | &__horizontal-img-container {
350 | display: flex;
351 | flex: auto;
352 | width:100%;
353 | margin-top:40px !important;
354 | margin-bottom:40px;
355 |
356 | align-items: center;
357 | justify-content: space-evenly;
358 | flex-direction: row;
359 | }
360 |
361 | &__horizontal-img {
362 | height: 30vh;
363 | }
364 |
365 | &__compare-block {
366 | height: calc(35vh + 32px + 20px + 70px);
367 | padding: 20px 0px 10px 0px;
368 | display: flex;
369 | flex: auto;
370 | align-items: center;
371 | justify-content: space-around;
372 | flex-direction: column;
373 |
374 | @media (max-width: 1128px) {
375 | height: auto;
376 | margin-top: 30px;
377 | }
378 | }
379 |
380 | &__solid {
381 | background-color: white;
382 | }
383 |
384 | &__options-box {
385 | margin: 5px 0px 5px 30px !important;
386 | align-items: center;
387 | display: flex;
388 |
389 | @media (max-width: 1128px) {
390 | margin: 5px auto !important;
391 | }
392 | }
393 |
394 | &__search {
395 | background-color: #f0f0f0 ;
396 | margin-bottom: 10px;
397 | flex-grow: 1;
398 | height: 60px;
399 | }
400 | }
401 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/_gen-art.scss:
--------------------------------------------------------------------------------
1 | // ---------------------------------------------------------------
2 | // This file contains all styles related to the .gen-art component
3 | // ---------------------------------------------------------------
4 | .gen-art {
5 | margin-right: $space-l;
6 | position: relative;
7 | width: $gen-size;
8 |
9 | @media (max-width: $mq-s){
10 | order: 2;
11 | }
12 |
13 | @media (max-width: $mq-ss) and (min-width: $mq-xs){
14 | height: auto;
15 | margin-right: 0px;
16 | width: 200px;
17 | }
18 |
19 | &__loader {
20 | align-items: center;
21 | background-color: $color-shade-light;
22 | display: flex;
23 | justify-content: center;
24 | height: $gen-size;
25 | margin-bottom: $space-m;
26 | position: relative;
27 | width: $gen-size;
28 |
29 | @media (min-width: $mq-xs){
30 | height: auto;
31 | width: auto;
32 | background-color: white;
33 | }
34 |
35 | img {
36 | display: block;
37 | // margin-left: auto;
38 | margin-right: auto;
39 | z-index: $z-index-s !important;
40 |
41 | @media (max-width: $mq-ss) and (min-width: $mq-xs){
42 | height: 200px;
43 | width: auto;
44 | }
45 | }
46 | }
47 |
48 | &__share{
49 | display: flex;
50 | align-items: flex-start;
51 | margin-bottom: 1em;
52 |
53 | .SocialMediaShareButton{
54 | margin-right:1em;
55 |
56 | :hover{
57 | cursor: pointer;
58 | }
59 | }
60 | }
61 |
62 | .button {
63 | margin-bottom: $space-m;
64 | width: 100%;
65 |
66 | @media (max-width: $mq-ss) and (min-width: $mq-xs) {
67 | width: 200px;
68 | font-size: $font-m*0.9;
69 | }
70 |
71 | @media (max-width: $mq-xs){
72 | width: calc(100% - 2rem);
73 | }
74 | }
75 |
76 | &__header {
77 | color: $color-base-dark;
78 | font-size: $font-l;
79 | font-weight: $font-weight-semibold;
80 | margin-bottom: .5rem;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/_grid-card.scss:
--------------------------------------------------------------------------------
1 | .grid-card {
2 | position: relative;
3 | margin-bottom: $space-l;
4 | width: 15% !important;
5 | height: auto !important;
6 | margin: 12px !important;
7 | padding-bottom: 20px;
8 | overflow: hidden;
9 |
10 | &:hover {
11 | box-shadow: 0px 0px 20px dimgrey;
12 | }
13 |
14 | @media (min-width: $mq-l) {
15 | }
16 |
17 | @media (max-width: $mq-l) {
18 | width: 15%;
19 | }
20 |
21 | @media (max-width: $mq-m) {
22 | width: 15%;
23 | }
24 |
25 | &__img {
26 | background-color: $color-shade-light;
27 | height: 15rem;
28 | max-height: 31.25rem;
29 | max-width: 31.25rem;
30 | margin-bottom: $space-s;
31 | // object-fit: contain;
32 | width: 100%;
33 |
34 | @media (max-width: $mq-m){
35 | height: 11rem;
36 | }
37 | }
38 |
39 | &__link {
40 | text-align: center;
41 | }
42 |
43 | &__title,
44 | &__text {
45 | margin-top: 0;
46 | padding: 0px 20px;
47 | }
48 |
49 | &__title {
50 | color: $color-primary;
51 | font-size: $font-l;
52 | font-weight: $font-weight-semibold;
53 | font-family: $font-family-heading;
54 | margin-bottom: $space-xs;
55 | @media (max-width: $mq-m) {
56 | font-size: $font-s;
57 | }
58 | @media (max-width: $mq-l) {
59 | font-size: $font-m;
60 | }
61 | }
62 |
63 | &__text {
64 | color: $color-primary;
65 | margin-bottom: 0;
66 | font-family: $font-family-base;
67 | @media (max-width: $mq-m) {
68 | font-size: $font-s;
69 | }
70 | @media (max-width: $mq-l) {
71 | font-size: $font-m;
72 | }
73 | }
74 |
75 | &__buttons {
76 | position: absolute;
77 | bottom: -30px;
78 | width: 100% !important;
79 | height: 30px;
80 | margin-top: auto !important;
81 | // box-shadow: 0px 5px 10px 5px gray;
82 | }
83 |
84 | &__slide-enter {
85 | bottom: -30px;
86 | }
87 |
88 | &__slide-enter-done {
89 | bottom: 0px;
90 | transition: bottom 0.15s linear;
91 | }
92 |
93 | &__slide-exit {
94 | bottom: 0px;
95 | }
96 |
97 | &__slide-exit-done {
98 | bottom: -30px;
99 | transition: bottom 0.25s linear;
100 | }
101 |
102 | &__button_link {
103 | display: inline-block;
104 | text-align: center;
105 | text-decoration: none;
106 | width: 100%;
107 | color: black;
108 | // background-color: gainsboro;
109 | background-color: rgb(240,240,240);
110 | font-family: $font-family-base;
111 | font-weight: 500;
112 | font-size: $font-m;
113 | line-height: 30px;
114 | }
115 |
116 | &__button_link:hover {
117 | background-color: gainsboro;
118 | }
119 |
120 | &__button_sep {
121 | width: 1px;
122 | height: 30px;
123 | background-color: gainsboro;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/_intro.scss:
--------------------------------------------------------------------------------
1 | .intro {
2 | box-shadow: $box-shadow;
3 | max-width: 70%;
4 | margin-left: auto;
5 | margin-right: auto;
6 |
7 |
8 | @include main-content_responsive();
9 |
10 | &__bg {
11 | background: #000;
12 | width: 100vw;
13 | height: 100vh;
14 | }
15 |
16 | &__topstack {
17 |
18 | @media (max-width: $mq-m) {
19 | margin-left: 10px;
20 | margin-right: 10px;
21 | }
22 | }
23 |
24 | &__center_image {
25 | display: block;
26 | margin-left: auto;
27 | margin-right: auto;
28 | width: 50%;
29 | }
30 |
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/_nav.scss:
--------------------------------------------------------------------------------
1 | // -------------------------------------------------------------
2 | // This file contains all styles related to the .nav component
3 | // -------------------------------------------------------------
4 | .nav {
5 | align-items: center;
6 | background-color: $color-shade-dark;
7 | display: flex;
8 | height: $nav-unit;
9 | justify-content: space-between;
10 | padding: 0px 0px;
11 | z-index: 10;
12 | width: 100vw;
13 | position:fixed;
14 | top:0;
15 | width:100%;
16 |
17 |
18 | @media (max-width: 1128px) {
19 | border-bottom: 1px solid gainsboro;
20 | }
21 |
22 | @media (max-width: $mq-s) {
23 | margin-bottom: $space-s;
24 | height: $nav-unit-s;
25 | }
26 |
27 | &__text_link {
28 | color: black;
29 | text-align: center;
30 | font-family: $font-family-base;
31 | font-size: 36px;
32 | font-weight: bold;
33 | text-decoration: none;
34 | margin-right: 40px;
35 | }
36 |
37 | &__menu_link {
38 | color: white;
39 | text-align: left;
40 | font-family: $font-family-base;
41 | font-size: 20px;
42 | font-weight: bold;
43 | text-decoration: none;
44 | margin-bottom: 10px;
45 | }
46 |
47 | &__link {
48 | align-items: center;
49 | display: flex;
50 | height: inherit;
51 | left: 0;
52 | margin-left: 20px;
53 | //padding-bottom: 10px;
54 | //margin-top: 5px;
55 |
56 |
57 | text-decoration: none;
58 | text-transform: uppercase;
59 |
60 | &:visited {
61 | color: black;
62 | }
63 |
64 | img{
65 | height: 3rem;
66 | //position: absolute;
67 | max-height: $nav-unit;
68 | @media (max-width: $mq-s) {
69 | max-height: 40px;
70 | }
71 | }
72 |
73 | span {
74 | color: $color-text-light;
75 | font-family: $font-family-heading;
76 | font-size: 1rem;
77 | font-weight: $font-weight-black;
78 | line-height: 0.9; // Magic string, do not change
79 | max-width: 60px; // Magic string, do not change
80 | //width: 100%;
81 | }
82 | }
83 |
84 | &__button {
85 | align-items: center;
86 | border: 1px solid $color-shade-dark;
87 | border-radius: 1.5rem;
88 | color: $color-shade-light;
89 | display: flex;
90 | height: 2.125rem;
91 | justify-content: space-between;
92 | padding-left: 1rem;
93 | padding-right: 1rem;
94 | text-decoration: none;
95 | width: 15rem;
96 | }
97 |
98 | &__text {
99 | font-size: $font-l;
100 | font-family: $font-family-heading;
101 | font-weight: $font-weight-bold;
102 | white-space: nowrap;
103 | text-decoration: none;
104 | position: absolute;
105 | }
106 |
107 | &__image {
108 | margin-left: 0.5rem;
109 | height: 1.25rem;
110 | width: 1.25rem;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/_original.scss:
--------------------------------------------------------------------------------
1 | .original {
2 | display: inline;
3 | max-width: 30%;
4 |
5 | @media (max-width: $mq-s){
6 | order: 3;
7 | max-width: calc(100% - 10rem);
8 | flex-direction: column;
9 | }
10 |
11 | @media (max-width: $mq-xs){
12 | display: none;
13 | }
14 |
15 | &__img {
16 | background-color: $color-shade-light;
17 | height: $original-img-size;
18 | margin-right: $space-m;
19 | object-fit: contain;
20 | width: $original-img-size;
21 |
22 | @media (max-width: $mq-s){
23 | align-self: end;
24 | height: auto;
25 | max-height: 256px;
26 | width: auto;
27 | }
28 | @media (max-width: $mq-ss){
29 | max-height: 200px;
30 | max-width: 100%;
31 | }
32 | }
33 |
34 | &__description,
35 | &__data {
36 | margin-top: 0;
37 | }
38 |
39 | &__description {
40 | color: $color-primary;
41 | font-size: $font-m;
42 | font-weight: $font-weight-semibold;
43 | margin-bottom: $space-m;
44 | }
45 |
46 | &__data {
47 | display: flex;
48 | margin-bottom: $space-xs;
49 | }
50 |
51 | &__title,
52 | &__text {
53 | font-size: $font-s;
54 | color: $color-primary;
55 | }
56 |
57 | &__title {
58 | font-weight: $font-weight-semibold;
59 | margin-right: $space-xs;
60 | }
61 |
62 | &__header{
63 | color: $color-base-dark;
64 | font-size: $font-m;
65 | font-weight: $font-weight-semibold;
66 | margin-bottom: .5rem;
67 |
68 | @media (max-width: $mq-s){
69 | font-size: $font-l;
70 | }
71 | }
72 |
73 | &__container{
74 | display: flex;
75 | flex-direction: row;
76 |
77 | @media (max-width: $mq-s){
78 | flex-direction: column;
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/_search.scss:
--------------------------------------------------------------------------------
1 | .search {
2 | box-shadow: $box-shadow;
3 | max-width: 70%;
4 | margin-left: auto;
5 | margin-right: auto;
6 |
7 | @include main-content_responsive();
8 |
9 | &__button {
10 | width: 100%;
11 | }
12 |
13 | &__section {
14 | padding: 10px;
15 | width: 200px;
16 | text-transform: capitalize;
17 |
18 | @media (max-width: $mq-l) {
19 | width: 135px;
20 | }
21 |
22 | @media (max-width: $mq-xs) {
23 | width: 100px;
24 | }
25 | }
26 |
27 | &__topstack {
28 | margin-left: 50px;
29 | margin-right: 50px;
30 | margin-top: 70px !important;
31 |
32 | @media (max-width: $mq-m) {
33 | margin-left: 10px;
34 | margin-right: 10px;
35 | }
36 | }
37 |
38 | &__content {
39 | display: flex;
40 | padding-bottom: 6rem;
41 | padding-left: $space-l;
42 | padding-right: $space-l;
43 | padding-top: $space-l;
44 |
45 | @media (max-width: $mq-m){
46 | padding-left: 1.5rem;
47 | padding-right: 1.5rem;
48 | padding-top: 1.5rem;
49 | }
50 | }
51 |
52 | &__input {
53 | background-color: $color-shade-light;
54 | background-image: url('../../images/icon-search.svg');
55 | background-position: 1.5625rem;
56 | background-repeat: no-repeat;
57 | background-size: 2.5rem;
58 | border: 0;
59 | height: 60px !important;
60 | padding-left: 5rem;
61 | font-family: $font-family-base;
62 | font-weight: bold;
63 | font-size: larger;
64 |
65 | @media (max-width: $mq-s) {
66 | height: 4rem;
67 | width: 85% !important;
68 | }
69 |
70 | &::-webkit-input-placeholder {
71 | color: $color-primary;
72 | font-size: $font-l;
73 | opacity: 0.8;
74 | }
75 | &::-moz-placeholder {
76 | color: $color-primary;
77 | font-size: $font-l;
78 | opacity: 0.8;
79 | }
80 | &:-ms-input-placeholder {
81 | color: $color-primary;
82 | font-size: $font-l;
83 | opacity: 0.8;
84 | }
85 |
86 | &:-moz-placeholder {
87 | color: $color-primary;
88 | font-size: $font-l;
89 | opacity: 0.8;
90 | }
91 | }
92 |
93 | &__row {
94 | display: flex;
95 | margin-left: $space-s;
96 | margin-bottom: $space-s;
97 | font-family: $font-family-base;
98 | }
99 |
100 | &__row_category {
101 | display: flex;
102 | margin-bottom: $space-s;
103 | margin-top: $space-m;
104 | font-family: $font-family-base;
105 |
106 | @media (max-width: $mq-m){
107 | margin-bottom: $space-s;
108 | margin-top: $space-s;
109 | }
110 | }
111 |
112 | &__checkbox {
113 | margin-top: $space-s;
114 | margin-right: $space-s;
115 | }
116 |
117 | &__label {
118 | display: block;
119 | max-width: $tags-size;
120 | overflow: hidden;
121 | text-overflow: ellipsis;
122 | white-space: pre-wrap;
123 | @media (max-width: $mq-m) {
124 | font-size: 14px
125 | }
126 | }
127 |
128 | &__tags {
129 | margin-right: $space-l;
130 | max-width: 15rem;
131 |
132 | @media (max-width: $mq-s){
133 | margin-right: $space-l;
134 | max-width: 25vw;
135 | }
136 | }
137 |
138 | &__grid {
139 | height: 0;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/frontend/src/styles/layout/_footer.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all styles related to the footer of the site/application.
3 | // -----------------------------------------------------------------------------
4 | .footer {
5 | font-size: $font-s;
6 | padding: $space-m;
7 | width: 100%;
8 | padding-top: $space-m;
9 | padding-bottom: $space-m;
10 |
11 | @media (max-width: $mq-l) {
12 | padding-left: 1rem;
13 | padding-right: 1rem;
14 | }
15 |
16 | @include container-centered();
17 |
18 |
19 | &__description {
20 | font-weight: $font-weight-semibold;
21 | margin-top: initial;
22 |
23 | span {
24 | margin-right: $space-xxs;
25 | }
26 | }
27 |
28 | &__divider {
29 | background-color: $color-shade-dark;
30 | height: 1px;
31 | margin-bottom: $space-m;
32 | }
33 |
34 | &__links {
35 | @media (min-width: $mq-s) {
36 | display: flex;
37 | justify-content: space-between;
38 | }
39 | }
40 |
41 | &__link {
42 | color: $color-primary;
43 | // display: block;
44 | font-weight: $font-weight-semibold;
45 | margin-right: $space-m;
46 | text-decoration: none;
47 | white-space: nowrap;
48 |
49 | @media (min-width: 33.75em) {
50 | display: initial;
51 | }
52 |
53 | &:hover {
54 | text-decoration: underline;
55 | }
56 |
57 | &--strong {
58 | color: $color-secondary;
59 | font-weight: $font-weight-semibold;
60 | margin-left: $space-xs;
61 | white-space: nowrap;
62 | }
63 | }
64 |
65 | &__lang {
66 | align-items: center;
67 | color: $color-secondary-hover;
68 | display: flex;
69 | margin-bottom: $space-s;
70 | white-space: nowrap;
71 |
72 | @media (min-width: $mq-s) {
73 | margin-bottom: initial;
74 | }
75 |
76 | span {
77 | font-weight: $font-weight-semibold;
78 | }
79 | }
80 |
81 | &__image {
82 | margin-right: $space-s;
83 | height: $icon-m;
84 | width: $icon-m;
85 | }
86 |
87 | &__text {
88 | font-weight: $font-weight-semibold;
89 | white-space: nowrap;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/frontend/src/styles/layout/_header.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains all styles related to the header of the site/application.
3 | // -----------------------------------------------------------------------------
4 |
--------------------------------------------------------------------------------
/frontend/src/styles/layout/_selectpage-head.scss:
--------------------------------------------------------------------------------
1 | .selectpage {
2 | &__head {
3 | display: flex;
4 | flex-wrap: wrap;
5 | height: 94px;
6 | justify-content: space-between;
7 | align-items: center;
8 | margin-bottom: $space-xl;
9 | padding-left: $space-m;
10 | padding-right: $space-m;
11 |
12 | @media (max-width: $mq-s) {
13 | height: auto;
14 | }
15 |
16 | @include container-centered();
17 | }
18 |
19 | .gAXUqz {
20 | border-color: transparent !important; // Overwrites dinamic class added by grommet library
21 | box-shadow: none !important; // Overwrites dinamic class added by grommet library
22 | }
23 |
24 | &__head button {
25 | align-self: self-end;
26 | border-color: $color-primary;
27 | box-shadow: 0 0 0px 0px $color-primary;
28 | }
29 |
30 | &__head button > div {
31 | border: 2px solid $color-primary;
32 | border-radius: 0;
33 | width: 300px;
34 |
35 | @media (max-width: $mq-s){
36 | width: auto;
37 | }
38 |
39 | svg {
40 | stroke: $color-grey;
41 | }
42 |
43 | input {
44 | border-radius: 0;
45 | color: $color-primary;
46 |
47 | &::-webkit-input-placeholder {
48 | // Chrome/Opera/Safari
49 | color: inherit;
50 | font-weight: $font-weight-normal;
51 | }
52 |
53 | &::-moz-placeholder {
54 | // Firefox 19+
55 | color: inherit;
56 | font-weight: $font-weight-normal;
57 | opacity: 1;
58 | }
59 |
60 | &:-ms-input-placeholder {
61 | // IE 10+
62 | color: inherit;
63 | font-weight: $font-weight-normal;
64 | }
65 |
66 | &:-moz-placeholder {
67 | // Firefox 18-
68 | color: inherit;
69 | font-weight: $font-weight-normal;
70 | }
71 | }
72 | }
73 | }
74 |
75 | .selectpage__head + div + div > .button {
76 | display: inline-block;
77 | margin-bottom: $space-xl;
78 | }
79 |
80 | .czusFq{
81 | background: $color-grey !important;
82 | }
83 |
--------------------------------------------------------------------------------
/frontend/src/styles/pages/_about.scss:
--------------------------------------------------------------------------------
1 | .about {
2 | &__panel {
3 | margin: 0px 20px;
4 | padding: 0px 10px;
5 | }
6 |
7 | &__pivot {
8 | padding: 10px 0px;
9 |
10 | }
11 | }
--------------------------------------------------------------------------------
/frontend/src/styles/pages/_explore.scss:
--------------------------------------------------------------------------------
1 | .explore {
2 | width: calc(100% - 18.5rem);
3 |
4 | @media (max-width: $mq-s){
5 | order: 4;
6 | width: 100%;
7 | }
8 |
9 | &__result {
10 | display: flex;
11 |
12 |
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/styles/pages/_map.scss:
--------------------------------------------------------------------------------
1 | // -----------------------------------------------------------------------------
2 | // This file contains styles that are specific to the map page.
3 | // -----------------------------------------------------------------------------
4 |
5 | .map {
6 | box-shadow: $box-shadow;
7 | margin-bottom: 4.5rem;
8 | margin-left: auto;
9 | margin-right: auto;
10 | max-width: 75%;
11 |
12 | @include main-content_responsive();
13 |
14 | &__header {
15 | font-weight: $font-weight-semibold;
16 | letter-spacing: 3.43px;
17 | }
18 |
19 | &__tab {
20 | background-color: $color-shade-light;
21 | border: none;
22 | color: $color-primary;
23 | display: inline-block;
24 | padding: $space-m $space-l;
25 | text-align: center;
26 | text-decoration: none;
27 | text-transform: uppercase;
28 | // width: 7.75rem;
29 |
30 | @media (max-width: $mq-ss)
31 | {
32 | width: 12rem;
33 | }
34 |
35 | @media (max-width: $mq-s)
36 | {
37 | width: 10rem;
38 | }
39 |
40 | @media (max-width: $mq-xs)
41 | {
42 | width: 8.5rem;
43 | padding: $space-m $space-m;
44 | }
45 |
46 |
47 | &:hover {
48 | cursor: pointer;
49 | }
50 |
51 | &.is-active {
52 | background-color: $color-secondary;
53 | color: $color-text-light;
54 | }
55 | }
56 |
57 | &__box {
58 | padding-top: $space-xl;
59 | padding-bottom: 6rem;
60 | padding-left: $space-xl;
61 | padding-right: $space-xl;
62 |
63 | @media (max-width: $mq-s){
64 | padding-left: $space-l;
65 | padding-right: $space-l;
66 | }
67 | }
68 |
69 | &__data {
70 | border-bottom: 1px solid $color-shade-dark;
71 | display: flex;
72 | flex-wrap: wrap;
73 | padding-bottom: 5rem;
74 | @media (max-width: $mq-s){
75 | justify-content: space-between;
76 | }
77 | }
78 |
79 | .claim {
80 | text-transform: uppercase;
81 |
82 | @media (max-width: $mq-xs){
83 | width: auto;
84 | }
85 | }
86 |
87 | &__description {
88 | color: $color-primary;
89 | font-size: $font-l;
90 | line-height: 1.78;
91 | margin-top: 0;
92 | margin-right: $space-l;
93 | margin-bottom: 3rem;
94 | width: calc(70% - #{$space-l});
95 |
96 | > p {
97 | margin-top: 0;
98 | }
99 |
100 | @media (max-width: $mq-s){
101 | width: 100%;
102 | margin-right: 0px;
103 | }
104 | }
105 |
106 | &__result {
107 | display: flex;
108 | }
109 |
110 | &__result-header {
111 | background-color: $color-primary;
112 | color: white;
113 | text-align: center;
114 | }
115 |
116 | &__plot {
117 | width: calc(100% - #{$space-l} - #{$gen-size});
118 |
119 | @media (max-width: $mq-s){
120 | order: 4;
121 | width: 100%;
122 | }
123 | }
124 |
125 | &__plot-header {
126 | background-color: $color-white;
127 | border: 1px $color-primary solid;
128 | color: $color-primary;
129 | margin-bottom: 1.5rem;
130 | padding-bottom: $space-xs;
131 | padding-top: $space-xs;
132 | text-align: center;
133 | }
134 |
135 | &__plot-graph {
136 | border-collapse: collapse;
137 | height: 28.125rem;
138 | margin: 0;
139 | pad: 0;
140 | padding: 0;
141 | width: 100%;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/frontend/src/styles/vendor/_normalize.scss:
--------------------------------------------------------------------------------
1 | //
2 | // ==========================================================================
3 | // DOCUMENT
4 | // normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css
5 | // ==========================================================================
6 | //
7 |
8 | //
9 | // ==========================================================================
10 | // Sections
11 | // ==========================================================================
12 | //
13 |
14 | //
15 | // Remove the margin in all browsers.
16 | //
17 |
18 | body {
19 | margin: 0;
20 | }
21 |
22 | //
23 | // Correct the font size and margin on `h1` elements within `section` and
24 | // `article` contexts in Chrome, Firefox, and Safari.
25 | //
26 |
27 | h1 {
28 | font-size: 2em;
29 | margin: 0.67em 0;
30 | }
31 |
32 | //
33 | // ==========================================================================
34 | // Grouping content
35 | // ==========================================================================
36 | //
37 |
38 | //
39 | // 1. Add the correct box sizing in Firefox.
40 | // 2. Show the overflow in Edge and IE.
41 | //
42 |
43 | hr {
44 | box-sizing: content-box; // 1
45 | height: 0; // 1
46 | overflow: visible; // 2
47 | }
48 |
49 | //
50 | // 1. Correct the inheritance and scaling of font size in all browsers.
51 | // 2. Correct the odd `em` font sizing in all browsers.
52 | //
53 |
54 | pre {
55 | font-family: monospace, monospace; // 1
56 | font-size: 1em; // 2
57 | }
58 |
59 | //
60 | // ==========================================================================
61 | // Text-level semantics
62 | // ==========================================================================
63 | //
64 |
65 | //
66 | // Remove the gray background on active links in IE 10.
67 | //
68 |
69 | a {
70 | background-color: transparent;
71 | }
72 |
73 | //
74 | // 1. Remove the bottom border in Chrome 57-
75 | // 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
76 | //
77 |
78 | abbr[title] {
79 | border-bottom: none; // 1
80 | text-decoration: underline; // 2
81 | text-decoration: underline dotted; // 2
82 | }
83 |
84 | //
85 | // Add the correct font weight in Chrome, Edge, and Safari.
86 | //
87 |
88 | b,
89 | strong {
90 | font-weight: bolder;
91 | }
92 |
93 | //
94 | // 1. Correct the inheritance and scaling of font size in all browsers.
95 | // 2. Correct the odd `em` font sizing in all browsers.
96 | //
97 |
98 | code,
99 | kbd,
100 | samp {
101 | font-family: monospace, monospace; // 1
102 | font-size: 1em; // 2
103 | }
104 |
105 | //
106 | // Add the correct font size in all browsers.
107 | //
108 |
109 | small {
110 | font-size: 80%;
111 | }
112 |
113 | //
114 | // Prevent `sub` and `sup` elements from affecting the line height in
115 | // all browsers.
116 | //
117 |
118 | sub,
119 | sup {
120 | font-size: 75%;
121 | line-height: 0;
122 | position: relative;
123 | vertical-align: baseline;
124 | }
125 |
126 | sub {
127 | bottom: -0.25em;
128 | }
129 |
130 | sup {
131 | top: -0.5em;
132 | }
133 |
134 | //
135 | // ==========================================================================
136 | // Embedded content
137 | // ==========================================================================
138 | //
139 |
140 | //
141 | // Remove the border on images inside links in IE 10.
142 | //
143 |
144 | img {
145 | border-style: none;
146 | }
147 |
148 | //
149 | // ==========================================================================
150 | // Forms
151 | // ==========================================================================
152 | //
153 |
154 | //
155 | // 1. Change the font styles in all browsers.
156 | // 2. Remove the margin in Firefox and Safari.
157 | //
158 |
159 | button,
160 | input,
161 | optgroup,
162 | select,
163 | textarea {
164 | font-family: inherit; // 1
165 | font-size: 100%; // 1
166 | line-height: 1.15; // 1
167 | margin: 0; // 2
168 | }
169 |
170 | //
171 | // Show the overflow in IE.
172 | // 1. Show the overflow in Edge.
173 | //
174 |
175 | button,
176 | input {
177 | overflow: visible; // 1
178 | }
179 |
180 | //
181 | // Remove the inheritance of text transform in Edge, Firefox, and IE.
182 | // 1. Remove the inheritance of text transform in Firefox.
183 | //
184 |
185 | button,
186 | select {
187 | text-transform: none; // 1
188 | }
189 |
190 | //
191 | // Correct the inability to style clickable types in iOS and Safari.
192 | //
193 |
194 | button,
195 | [type='button'],
196 | [type='reset'],
197 | [type='submit'] {
198 | -webkit-appearance: button;
199 | }
200 |
201 | //
202 | // Remove the inner border and padding in Firefox.
203 | //
204 |
205 | button::-moz-focus-inner,
206 | [type='button']::-moz-focus-inner,
207 | [type='reset']::-moz-focus-inner,
208 | [type='submit']::-moz-focus-inner {
209 | border-style: none;
210 | padding: 0;
211 | }
212 |
213 | //
214 | // Restore the focus styles unset by the previous rule.
215 | //
216 |
217 | button:-moz-focusring,
218 | [type='button']:-moz-focusring,
219 | [type='reset']:-moz-focusring,
220 | [type='submit']:-moz-focusring {
221 | outline: 1px dotted ButtonText;
222 | }
223 |
224 | //
225 | // Correct the padding in Firefox.
226 | //
227 |
228 | fieldset {
229 | padding: 0.35em 0.75em 0.625em;
230 | }
231 |
232 | //
233 | // 1. Correct the text wrapping in Edge and IE.
234 | // 2. Correct the color inheritance from `fieldset` elements in IE.
235 | // 3. Remove the padding so developers are not caught out when they zero out
236 | // `fieldset` elements in all browsers.
237 | //
238 |
239 | legend {
240 | box-sizing: border-box; // 1
241 | color: inherit; // 2
242 | display: table; // 1
243 | max-width: 100%; // 1
244 | padding: 0; // 3
245 | white-space: normal; // 1
246 | }
247 |
248 | //
249 | // Add the correct vertical alignment in Chrome, Firefox, and Opera.
250 | //
251 |
252 | progress {
253 | vertical-align: baseline;
254 | }
255 |
256 | //
257 | // Remove the default vertical scrollbar in IE 10+.
258 | //
259 |
260 | textarea {
261 | overflow: auto;
262 | }
263 |
264 | //
265 | // 1. Add the correct box sizing in IE 10.
266 | // 2. Remove the padding in IE 10.
267 | //
268 |
269 | [type='checkbox'],
270 | [type='radio'] {
271 | box-sizing: border-box; /* 1 */
272 | padding: 0; /* 2 */
273 | }
274 |
275 | //
276 | // Correct the cursor style of increment and decrement buttons in Chrome.
277 | //
278 |
279 | [type='number']::-webkit-inner-spin-button,
280 | [type='number']::-webkit-outer-spin-button {
281 | height: auto;
282 | }
283 |
284 | //
285 | // 1. Correct the odd appearance in Chrome and Safari.
286 | // 2. Correct the outline style in Safari.
287 | //
288 |
289 | [type='search'] {
290 | -webkit-appearance: textfield; // 1
291 | outline-offset: -2px; // 2
292 | }
293 |
294 | //
295 | // Remove the inner padding in Chrome and Safari on macOS.
296 | //
297 |
298 | [type='search']::-webkit-search-decoration {
299 | -webkit-appearance: none;
300 | }
301 |
302 | //
303 | // 1. Correct the inability to style clickable types in iOS and Safari.
304 | // 2. Change font properties to `inherit` in Safari.
305 | //
306 |
307 | ::-webkit-file-upload-button {
308 | -webkit-appearance: button; // 1
309 | font: inherit; // 2
310 | }
311 |
312 | //
313 | // ==========================================================================
314 | // Interactive
315 | // ==========================================================================
316 | //
317 |
318 | //
319 | // Add the correct display in Edge, IE 10+, and Firefox.
320 | //
321 |
322 | details {
323 | display: block;
324 | }
325 |
326 | //
327 | // Add the correct display in all browsers.
328 | //
329 |
330 | summary {
331 | display: list-item;
332 | }
333 |
334 | //
335 | // ==========================================================================
336 | // Misc
337 | // ==========================================================================
338 | //
339 | //
340 |
341 | //
342 | // Add the correct display in IE 10+.
343 | //
344 |
345 | template {
346 | display: none;
347 | }
348 |
349 | //
350 | // Add the correct display in IE 10.
351 | //
352 |
353 | [hidden] {
354 | display: none;
355 | }
356 |
--------------------------------------------------------------------------------
/frontend/src/styles/vendor/_slick.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Slider
3 | //
4 | .slick-slider {
5 | position: relative;
6 | display: block;
7 | box-sizing: border-box;
8 | -webkit-touch-callout: none;
9 | user-select: none;
10 | -ms-touch-action: pan-y;
11 | overflow: hidden;
12 | touch-action: pan-y;
13 | -webkit-tap-highlight-color: transparent;
14 |
15 | @media (max-width: $mq-s) {
16 | max-width: 100%;
17 | }
18 |
19 | &:hover {
20 | cursor: pointer;
21 | }
22 | }
23 |
24 | .slick-list {
25 | position: relative;
26 | overflow: hidden;
27 | display: block;
28 | margin: 0;
29 | padding: 0;
30 | height: calc(max(20vh, 200px) + 8px) !important;
31 |
32 | &:focus {
33 | outline: 0;
34 | }
35 |
36 | &.dragging {
37 | cursor: pointer;
38 | cursor: hand;
39 | }
40 | }
41 |
42 | .slick-slider .slick-track,
43 | .slick-slider .slick-list {
44 | transform: translate3d(0, 0, 0);
45 | }
46 |
47 | .slick-track {
48 | position: relative;
49 | left: 0;
50 | top: 0;
51 | display: block;
52 | margin-left: auto;
53 | margin-right: auto;
54 |
55 | &:before,
56 | &:after {
57 | content: '';
58 | display: table;
59 | }
60 |
61 | &:after {
62 | clear: both;
63 | }
64 |
65 | .slick-loading & {
66 | visibility: hidden;
67 | }
68 | }
69 |
70 | .slick-slide {
71 | float: left;
72 | height: 100%;
73 | min-height: 1px;
74 |
75 | [dir='rtl'] & {
76 | float: right;
77 | }
78 |
79 | img {
80 | display: block;
81 | }
82 |
83 | &.slick-loading img {
84 | display: none;
85 | }
86 |
87 | display: none;
88 |
89 | &.dragging img {
90 | pointer-events: none;
91 | }
92 |
93 | .slick-initialized & {
94 | display: block;
95 | }
96 |
97 | .slick-loading & {
98 | visibility: hidden;
99 | }
100 |
101 | .slick-vertical & {
102 | display: block;
103 | height: auto;
104 | border: 1px solid purple;
105 | }
106 | }
107 |
108 | .slick-arrow.slick-hidden {
109 | display: none;
110 | }
111 |
112 | //
113 | // Custom Styles
114 | //
115 | .slick-slide button {
116 | font-size: 36px;
117 | line-height: 100px;
118 | margin: 10px;
119 | padding: 2%;
120 | position: relative;
121 | text-align: center;
122 | }
123 |
124 | .variable-width .slick-slide p {
125 | background-color: $color-slide-background;
126 | height: 100px;
127 | color: #fff;
128 | margin: 5px;
129 | line-height: 100px;
130 | text-align: center;
131 | }
132 |
133 | .center {
134 | .slick-center button {
135 | opacity: 1;
136 | transform: scale(1.08);
137 | }
138 |
139 | button {
140 | transition: all 300ms ease;
141 | }
142 | }
143 |
144 | .content {
145 | padding: 20px;
146 | margin: auto;
147 |
148 | @media (min-width: 701px) {
149 | width: 80%;
150 | }
151 |
152 | @media (max-width: 700px) {
153 | width: 70%;
154 | }
155 | }
156 |
157 | .slick-slide {
158 | .image {
159 | padding: 10px;
160 | }
161 |
162 | img.slick-loading {
163 | border: 0;
164 | }
165 | }
166 |
167 | .slick-slider {
168 | margin: 30px auto 30px;
169 | }
170 |
171 | .slick-dots {
172 | margin-left: 0;
173 | }
174 |
175 | .slick-thumb {
176 | bottom: -45px;
177 |
178 | li {
179 | width: 60px;
180 | height: 45px;
181 | }
182 |
183 | li img {
184 | width: 100%;
185 | height: 100%;
186 | filter: grayscale(100%);
187 | }
188 |
189 | li.slick-active img {
190 | filter: grayscale(0%);
191 | }
192 | }
193 |
194 | @media (max-width: 768px) {
195 | .slick-slide button {
196 | font-size: 24px;
197 | }
198 |
199 | // .center {
200 | // margin-left: -40px;
201 | // margin-right: -40px;
202 | // }
203 |
204 | .center .slick-center button {
205 | color: $color-slide-text;
206 | opacity: 1;
207 | transform: scale(1);
208 | }
209 |
210 | .center button {
211 | transition: all 300ms ease;
212 | @media (min-width: $mq-s) {
213 | transform: scale(0.95);
214 | }
215 | }
216 | }
217 |
218 | .slick-vertical .slick-slide {
219 | height: 180px;
220 | }
221 |
222 | //
223 | // Arrows
224 | //
225 | .slick-arrow {
226 | background-color: $color-arrow-background;
227 | border: 2px solid transparent;
228 | border-radius: calc(#{$arrow-size} / 2);
229 | box-shadow: 0 2px 20px 0 rgba(0, 32, 80, 0.3);
230 | color: transparent;
231 | font-weight: $font-weight-semibold;
232 | height: $arrow-size;
233 | text-transform: uppercase;
234 | position: relative;
235 | width: $arrow-size;
236 | margin: 0 15px;
237 | @media (max-width: $mq-s) {
238 | height: $arrow-size - 2rem;
239 | width: $arrow-size - 2rem;
240 | }
241 |
242 | @include transition(background-color);
243 |
244 | // &:hover {
245 | // background-color: $color-primary;
246 | // }
247 |
248 | &:focus, &:hover {
249 | background-color: $color-primary;
250 | border: 2px solid $color-primary;
251 | outline: 0;
252 | }
253 |
254 | &.slick-prev,
255 | &.slick-next {
256 | font-size: 1px;
257 | position: absolute;
258 | top: 50%;
259 | transform: translateY(-50%);
260 | z-index: $z-index-xl;
261 |
262 | &:after {
263 | display: block;
264 | // height: 2.375rem;
265 | // height: 100%;
266 | // width: 1.25rem;
267 | // width: 100%;
268 | height: $arrow-size;
269 | width: $arrow-size;
270 | }
271 | }
272 |
273 | &.slick-next {
274 | right: 0;
275 |
276 | @media (min-width: $mq-s) {
277 | right: 0;
278 | }
279 |
280 | &:after {
281 | content: '';
282 | background-image: url('../../images/icon-chevron-right.svg');
283 | background-repeat: no-repeat;
284 | background-position: -10px -40px;
285 | background-size: $arrow-size;
286 | @media (max-width: $mq-s) {
287 | background-position: -8px -24px;
288 | background-size: $arrow-size - 2rem;
289 | }
290 | }
291 |
292 | &:focus:after, &:hover:after {
293 | content: '';
294 | background-image: url('../../images/icon-chevron-right-inverted.svg');
295 | background-repeat: no-repeat;
296 | background-position: -10px -40px;
297 | background-size: $arrow-size;
298 | @media (max-width: $mq-s) {
299 | background-position: -8px -24px;
300 | background-size: $arrow-size - 2rem;
301 | }
302 | }
303 | }
304 |
305 | &.slick-prev {
306 | left: 0;
307 |
308 | @media (min-width: $mq-s) {
309 | left: 0;
310 | }
311 |
312 | &:after {
313 | content: '';
314 | background-image: url('../../images/icon-chevron-left.svg');
315 | background-repeat: no-repeat;
316 | background-position: -10px -40px;
317 | background-size: $arrow-size;
318 | @media (max-width: $mq-s) {
319 | background-position: -8px -24px;
320 | background-size: $arrow-size - 2rem;
321 | }
322 | }
323 |
324 | &:focus:after,&:hover:after {
325 | content: '';
326 | background-image: url('../../images/icon-chevron-left-inverted.svg');
327 | background-repeat: no-repeat;
328 | background-position: -10px -40px;
329 | background-size: $arrow-size;
330 | @media (max-width: $mq-s) {
331 | background-position: -8px -24px;
332 | background-size: $arrow-size - 2rem;
333 | }
334 | }
335 | }
336 | }
337 |
338 | //
339 | // Slick additions
340 | //
341 | .button {
342 | padding: 10px 20px;
343 | margin: 0 20px;
344 | border: none;
345 | font-size: 20px;
346 | border-radius: 5px;
347 | min-height: 45px;
348 | }
349 |
350 | .slick-img {
351 | border: 0px solid #fff;
352 | display: block;
353 | margin: auto;
354 | max-width: 100%;
355 | // padding-left: 5%;
356 | // padding-right: 5%;
357 | object-fit: cover;
358 | height: 100%;
359 | width: auto;
360 | max-width: 300px;
361 | z-index: calc(#{$z-index-s} * -1);
362 |
363 | @media (max-width: $mq-xl) {
364 | max-width: 275px;
365 | }
366 |
367 | @media (max-width: $mq-m) {
368 | max-width: 250px;
369 | }
370 |
371 | @media (max-width: $mq-ss) {
372 | max-width: 225px;
373 | }
374 | }
375 |
376 | .slick-img-container {
377 | height: 98%;
378 | outline: none;
379 | }
380 |
381 | .slick-slide > div {
382 | height: auto; // Needed to show the full slide height, do not change
383 | //height: 340px;
384 |
385 | @media (max-width: $mq-ss){
386 | height: 275px;
387 | }
388 |
389 | button {
390 | height: inherit;
391 | margin-left: 50px;
392 | }
393 | img {
394 | height: inherit;
395 | }
396 | }
397 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "preserve"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/utils/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/frontend/utils/head_additions.html:
--------------------------------------------------------------------------------
1 | Mosaic: Find Hidden Artistic Connections with Deep Learning
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/frontend/utils/single_page_app.html:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/media/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/media/architecture.png
--------------------------------------------------------------------------------
/media/e2e.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/media/e2e.gif
--------------------------------------------------------------------------------
/media/header-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/media/header-image.jpg
--------------------------------------------------------------------------------
/media/match1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/media/match1.jpg
--------------------------------------------------------------------------------
/media/match2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/media/match2.jpg
--------------------------------------------------------------------------------
/media/mit_externs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/media/mit_externs.jpg
--------------------------------------------------------------------------------
/media/teaser_img.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/media/teaser_img.gif
--------------------------------------------------------------------------------
/media/webinar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/art/3ea0616a449cc1c03ee49f1777ce6b331c520b57/media/webinar.jpg
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "lockfileVersion": 1
3 | }
4 |
--------------------------------------------------------------------------------
/transparency_note.md:
--------------------------------------------------------------------------------
1 | # AI Fairness and Transparency
2 |
3 | Art, culture, and heritage are sensitive subjects that require respect.
4 | This work aims to celebrate culture and diversity and explore art in a new way.
5 | AI has known biases and the results or artworks within this tool do not reflect
6 | the views of Microsoft, MIT, or the authors. We ask users to be respectfull of other's cultures and to use this tool responsibly.
7 | Some artworks or matches might not be approporiate for all ages, or might be culturally inappropriate.
8 | We have released this tool as-is. Thanks!
9 |
10 | We note the following about our approach
11 |
12 | - Deep Image Networks might be biased towards particular shapes, forms, races, and cultures as the networks we use are trained on the ImageNet dataset, which might over- or underrepresent certain groups or demonstrate stereotypes.
13 |
14 | - The Artwork used in our approach is from the open access collections of the MET and Rijksmusem and represent a particular curatorial view that is not necessarily representative of the worlds art.
15 |
16 | - Artwork itself may be insensitive to some as morals and societal concepts of fairness vary across culture and time.
17 |
18 | - We can only use cultures and media from the art collections and this might be not very accurate or this information can be biased towards a particular culture.
19 |
20 | - We provide many matches for each image and allow users to match with any image in the collection and any culture. The flexibility of this tool can allow for many items to be matched together including ones that might be insensitive. For example you could search for an animal and then ask it to match against photos of people, the method will return the most visually similiar pair,but this does not mean that this should be interpreted as our algorithm saying person X is an animal/ looks like an animal. Likewise the method might find matches that are more random than anything else, thus we want to ensure people do not take the matches as definitive or as the only way two cultures are connected.
21 |
22 | - We want people to use this as a tool of cultural celebration and discovery!
23 |
--------------------------------------------------------------------------------