commit
c4b0608c4e
|
@ -3,6 +3,7 @@ node_modules
|
|||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
/backend/dist
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
## Collaborators
|
||||
|
||||
- [Xinrui Chen](github.com/xynree)
|
||||
- [Zane Schaffer](github.com/zschaffer)
|
39
README.md
39
README.md
|
@ -1,38 +1,7 @@
|
|||
# create-svelte
|
||||
# Rating Room
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
## About
|
||||
|
||||
## Creating a project
|
||||
This project is a collaborative effort to catalogue and rate everything in existence.
|
||||
Written in `Svelte` with `TailwindCSS` and `Sanity`.
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm init svelte
|
||||
|
||||
# create a new project in my-app
|
||||
npm init svelte my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
<!doctype html><html><head><meta charSet="utf-8"/><title>black-rail – Sanity</title><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/><meta name="robots" content="noindex"/><meta name="referrer" content="same-origin"/><style>html {background-color: #f1f3f6;}</style><link rel="stylesheet" href="/static/css/main.css?f07f88ced61604528519"/><link rel="subresource" href="/static/js/vendor.bundle.js?e48ae05b94fc36a66f3f"/><link rel="subresource" href="/static/js/app.bundle.js?f07f88ced61604528519"/><link rel="icon" href="/static/favicon.ico"/></head><body id="sanityBody"><div id="sanity"><div class="sanity-app-loading-screen"><style type="text/css">
|
||||
.sanity-app-loading-screen {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
color: #1C2430;
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sanity-app-loading-screen__inner {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.sanity-app-loading-screen__text {
|
||||
margin-top: 2rem;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sanity-app-loading-screen__loader {
|
||||
display: block;
|
||||
animation: pulse var(--time) cubic-bezier(.11,0,.27,1) infinite;
|
||||
--time: 3s;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
transform: scale3d(0.75, 0.75, 0.75);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale3d(0.75, 0.75, 0.75);
|
||||
}
|
||||
}
|
||||
</style><div class="sanity-app-loading-screen__inner"><div class="sanity-app-loading-screen__loader"><svg width="73" height="95" viewBox="0 0 73 95" fill="none" xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision"><style>
|
||||
.fillShape {
|
||||
stroke-width: 40;
|
||||
stroke: #F03E2F;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fillShape--bottom {
|
||||
stroke-dasharray: 90;
|
||||
stroke-dashoffset: 85;
|
||||
animation: bottom var(--time) ease-in infinite;
|
||||
}
|
||||
|
||||
.fillShape--middle {
|
||||
stroke-dasharray: 115;
|
||||
stroke-dashoffset: 110;
|
||||
animation: middle var(--time) linear infinite;
|
||||
}
|
||||
|
||||
.fillShape--top {
|
||||
stroke-dasharray: 77;
|
||||
stroke-dashoffset: 72;
|
||||
animation: top var(--time) ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes bottom {
|
||||
0%,
|
||||
85% {
|
||||
stroke-dashoffset: 265;
|
||||
opacity: 0;
|
||||
}
|
||||
15%,
|
||||
64% {
|
||||
stroke-dashoffset: 175;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
@keyframes middle {
|
||||
11%,
|
||||
75% {
|
||||
stroke-dashoffset: 100;
|
||||
opacity: 0;
|
||||
}
|
||||
15% {
|
||||
opacity: 1;
|
||||
}
|
||||
25%,
|
||||
63% {
|
||||
stroke-dashoffset: 225;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes top {
|
||||
22%,
|
||||
70% {
|
||||
opacity: 0;
|
||||
stroke-dashoffset: 226;
|
||||
}
|
||||
25% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
35%,
|
||||
54% {
|
||||
stroke-dashoffset: 149;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style><mask id="bottom" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="83" height="105"><path d="M56.4905 62.9807C63.6434 67.5637 66.7972 73.9744 66.7972 83.168C60.7987 90.7262 50.4577 95 38.2203 95C17.6963 95 3.17759 84.8102 0.0100098 67.282H19.7851C22.3343 75.3761 29.0748 79.1209 38.0622 79.1209C49.056 79.1209 56.3462 73.2598 56.4905 62.9807" fill="white"></path></mask><g mask="url(#bottom)"><path class="fillShape fillShape--bottom" d="M8 59C9.35605 77.6466 25.4128 87.4032 37.5 87.5C54 87.6322 62 75 71.5 66.5"></path></g><mask id="middle" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="83" height="105"><path d="M7.73317 12.1206C7.73317 24.8871 15.6418 32.5484 31.4729 36.568L48.2521 40.4571C63.238 43.8926 72.3834 52.4472 72.3834 66.3818C72.493 72.4436 70.5187 78.3594 66.7904 83.1404C66.7904 69.2402 59.5963 61.7232 42.2605 57.2157L25.7698 53.471C12.5842 50.4683 2.40806 43.4323 2.40806 28.2952C2.33733 22.4523 4.21324 16.7521 7.74004 12.0931" fill="white"></path></mask><g mask="url(#middle)"><path class="fillShape fillShape--middle" d="M-15.5 8.5C-15.5 12 -1 41.7047 38.3457 48C65.7344 52.3822 69 68 62.8457 74.5C56.6914 81 54.5 82 54.5 82"></path></g><mask id="top" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="83" height="105"><path d="M18.0122 31.1124C11.1411 26.7149 7.70557 20.524 7.70557 12.1138C13.4636 4.59677 23.3648 0 35.5198 0C56.4973 0 68.6317 11.09 71.6275 26.6737H52.5945C50.4988 20.5309 45.2562 15.7348 35.6641 15.7348C25.4399 15.7348 18.4863 21.6852 18.0122 31.1124" fill="white"></path></mask><g mask="url(#top)"><path class="fillShape fillShape--top" d="M9 35C9 21 19 5.49993 40 8.49996C61 11.5 65.3456 29 65.3456 33.5"></path></g></svg></div><div class="sanity-app-loading-screen__text">Connecting to Sanity.io</div></div></div><noscript><div class="sanity-app-no-js__root"><div class="sanity-app-no-js__content"><style type="text/css">
|
||||
.sanity-app-no-js__root {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sanity-app-no-js__content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
font-family: helvetica, arial, sans-serif;
|
||||
}
|
||||
</style><h1>JavaScript disabled</h1><p>Please <a href="https://www.enable-javascript.com/">enable JavaScript</a> in your browser and reload the page to proceed.</p></div></div></noscript></div><script>window.onerror = function(m,u,l,c,e) {var p=window.location.port;var h=window.location.protocol+'//'+window.location.hostname+(p?':'+p:'');var r=document.getElementById('sanity');while(r.firstChild){r.removeChild(r.firstChild);}var s=document.createElement('style');s.appendChild(document.createTextNode('html,body,#sanityBody,#sanity,#sanityError{height:100%;}body{-webkit-font-smoothing:antialiased;margin:0;}#sanityError{position:fixed;top:0;left:0;width:100%;height:100%;overflow:auto;background-color:#fff;color:#121923;font-family:-apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, \'Helvetica Neue\', Helvetica, Arial, system-ui, sans-serif;font-size:16px;line-height:21px;min-height:100%;}#sanityError>div{background-color:#fff;max-width:960px;margin:0 auto;padding:47px 32px 52px;}@media(min-width:512px){#sanityError>div{padding:47px 84px;}}#sanityError button{-webkit-font-smoothing:inherit;font:inherit;font-weight:500;background-color:#2276FC;color:#fff;padding:7px 12px;border-radius:3px;border:0;}#sanityError button:hover{background-color:#1E63D0;}#sanityError button:active{background-color:#1B50A5;}'));document.head.appendChild(s);var f=document.createElement('div');f.id='sanityError';f.innerHTML='<div><h1 style="font-size:21px;line-height:27px;margin: 0 0 10px;">Unhandled error</h1><p style="color:#66758D;margin:10px 0 14px;">Encountered an unhandled error in this Studio.</p><button class="sanity-error-handler__reload-btn" type="button">Reload page</button><pre style="background-color:#FDEBEA;color:#C3362C;font-size:13px;line-height:17px;padding:8px 12px;border-radius:3px;margin:32px 0 0;overflow:auto;"><code style="font-family:-apple-system-ui-monospace, \'SF Mono\', Menlo, Monaco, Consolas, monospace;">'+e.stack.replaceAll(h,'')+'</code></pre></div>';var b=f.querySelector('.sanity-error-handler__reload-btn');if(b){b.onclick=function() {window.location.reload();}};r.appendChild(f);};</script><script>/* Script loader */
|
||||
!function(e,t){if(void 0!==window.fetch)t.forEach(function(t){var o=e.createElement("script");o.src=t,o.async=!1,e.head.appendChild(o)});else{var o=e.getElementById("sanity");'<div style="height:100%;width: 100%;position: absolute;top:45%;text-align:center;font-family:helvetica, arial, sans-serif;">',"<h1>Browser not supported</h1>",'<p>Please use a modern browser such as <a href="https://www.google.com/chrome/">Chrome</a> or <a href="https://www.getfirefox.org/">Firefox</a>.</p>',"</div>","</div>";var r=o;do{r.style.height="100%",r=r.parentNode}while(r.parentNode);o.innerHTML='<div style="position:relative;height:100%;"><div style="height:100%;width: 100%;position: absolute;top:45%;text-align:center;font-family:helvetica, arial, sans-serif;"><h1>Browser not supported</h1><p>Please use a modern browser such as <a href="https://www.google.com/chrome/">Chrome</a> or <a href="https://www.getfirefox.org/">Firefox</a>.</p></div></div>'}}(document,["/static/js/vendor.bundle.js?e48ae05b94fc36a66f3f","/static/js/app.bundle.js?f07f88ced61604528519"]);</script></body></html>
|
|
@ -1 +0,0 @@
|
|||
Files placed here will be served by the Sanity server under the `/static`-prefix
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -10,6 +10,7 @@
|
|||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@sanity/base": "^2.30.1",
|
||||
"@sanity/client": "^3.3.2",
|
||||
"@sanity/core": "^2.30.2",
|
||||
"@sanity/default-layout": "^2.30.1",
|
||||
"@sanity/default-login": "^2.30.1",
|
||||
|
@ -22,7 +23,9 @@
|
|||
"react-dom": "^17.0",
|
||||
"styled-components": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.18.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.2.0",
|
||||
|
@ -1649,7 +1652,6 @@
|
|||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.6.tgz",
|
||||
"integrity": "sha512-WrthhuIIYKrEFAwttYzgRNQ5hULGmwTj+D6l7Zdfsv5M7IWV/OZbUfbeL++Qrzx1nVJwWROIFhCHRYQV4xbPNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.18.6",
|
||||
"@babel/helper-compilation-targets": "^7.18.6",
|
||||
|
@ -2391,7 +2393,6 @@
|
|||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@sanity/client/-/client-3.3.2.tgz",
|
||||
"integrity": "sha512-M89v/KcNob0tnoMCW2caSnwYhSSqO1j5XuDXCloe966ZRFUAZWe9EVyyvvP7x1vPff5xR9PKxidrcx95l221mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sanity/eventsource": "^4.0.0",
|
||||
"@sanity/generate-help-url": "^3.0.0",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@sanity/base": "^2.30.1",
|
||||
"@sanity/client": "^3.3.2",
|
||||
"@sanity/core": "^2.30.2",
|
||||
"@sanity/default-layout": "^2.30.1",
|
||||
"@sanity/default-login": "^2.30.1",
|
||||
|
@ -27,9 +28,11 @@
|
|||
"react-dom": "^17.0",
|
||||
"styled-components": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:zschaffer/rating-room.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.18.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
export default {
|
||||
title: 'Emotions',
|
||||
name: 'emotion',
|
||||
type: 'document',
|
||||
description: 'Emotions that our main characters can display',
|
||||
fields: [
|
||||
{
|
||||
title: 'Emotion name',
|
||||
name: 'name',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
name: 'description',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
title: 'Zane image',
|
||||
name: 'zane',
|
||||
type: 'image'
|
||||
},
|
||||
{
|
||||
title: 'Xinrui image',
|
||||
name: 'xyn',
|
||||
type: 'image'
|
||||
},
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'name',
|
||||
subtitle: 'description'
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,109 @@
|
|||
const ratings = {
|
||||
title: 'Ratings',
|
||||
name: 'rating',
|
||||
type: 'array',
|
||||
of: [
|
||||
{
|
||||
type: 'object', fields: [
|
||||
{
|
||||
title: 'Rater Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
validation: Rule => Rule.required(),
|
||||
options: {
|
||||
list: [
|
||||
{ title: 'Xinrui', value: 'xyn' },
|
||||
{ title: 'Zane', value: 'zane' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Rating Number',
|
||||
name: 'rating',
|
||||
type: 'number',
|
||||
options: {
|
||||
list: [
|
||||
{ title: '⭐', value: 1 },
|
||||
{ title: '⭐⭐', value: 2 },
|
||||
{ title: '⭐⭐⭐', value: 3 },
|
||||
{ title: '⭐⭐⭐⭐', value: 4 },
|
||||
{ title: '⭐⭐⭐⭐⭐', value: 5 }
|
||||
]
|
||||
},
|
||||
validation: Rule => Rule.required()
|
||||
},
|
||||
{
|
||||
title: 'Comments',
|
||||
name: 'comments',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
title: 'Rating Slug',
|
||||
name: 'slug',
|
||||
type: 'slug',
|
||||
options: {
|
||||
source: (doc, {parent}) => {
|
||||
console.log(doc, parent)
|
||||
return `${parent.name}_${parent.rating}_${doc.name}_${doc._id.slice(-6)}`}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Emotion',
|
||||
name: 'emotion',
|
||||
type: 'reference',
|
||||
to: [{type: 'emotion'}]
|
||||
}
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'name',
|
||||
subtitle: 'slug.current'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
export default {
|
||||
title: 'Products',
|
||||
name: 'product',
|
||||
type: 'document',
|
||||
description: 'List of all rated products',
|
||||
preview: {
|
||||
select: {
|
||||
title: 'name',
|
||||
subtitle: 'description'
|
||||
}
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
title: 'Name of Product',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
validation: (Rule) => Rule.required()
|
||||
},
|
||||
{
|
||||
title: 'Upload an Image',
|
||||
name: 'image',
|
||||
type: 'image'
|
||||
},
|
||||
{
|
||||
title: 'Select Tags',
|
||||
name: 'tags',
|
||||
type: 'array',
|
||||
of: [{
|
||||
type: 'reference',
|
||||
to: [{type: 'tag'}]
|
||||
}]
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Description of Product',
|
||||
name: 'description',
|
||||
type: 'text'
|
||||
},
|
||||
ratings,
|
||||
|
||||
]
|
||||
};
|
|
@ -4,6 +4,9 @@ import createSchema from 'part:@sanity/base/schema-creator'
|
|||
// Then import schema types from any plugins that might expose them
|
||||
import schemaTypes from 'all:part:@sanity/base/schema-type'
|
||||
|
||||
import products from './products'
|
||||
import emotions from './emotions'
|
||||
import tags from './tags'
|
||||
// Then we give our schema to the builder and provide the result to Sanity
|
||||
export default createSchema({
|
||||
// We name our schema
|
||||
|
@ -11,6 +14,7 @@ export default createSchema({
|
|||
// Then proceed to concatenate our document type
|
||||
// to the ones provided by any plugins that are installed
|
||||
types: schemaTypes.concat([
|
||||
products, emotions, tags
|
||||
/* Your types here! */
|
||||
]),
|
||||
})
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
export default {
|
||||
title: 'Tags',
|
||||
name: 'tag',
|
||||
type: 'document',
|
||||
fields: [
|
||||
{
|
||||
title: 'Tag name',
|
||||
name: 'name',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
title: 'Image',
|
||||
name: 'image',
|
||||
type: 'image',
|
||||
}
|
||||
|
||||
]
|
||||
}
|
|
@ -831,7 +831,7 @@
|
|||
"@babel/helper-create-regexp-features-plugin" "^7.18.6"
|
||||
"@babel/helper-plugin-utils" "^7.18.6"
|
||||
|
||||
"@babel/preset-env@^7.11.5", "@babel/preset-env@^7.16.8":
|
||||
"@babel/preset-env@^7.11.5", "@babel/preset-env@^7.16.8", "@babel/preset-env@^7.18.6":
|
||||
"integrity" "sha512-WrthhuIIYKrEFAwttYzgRNQ5hULGmwTj+D6l7Zdfsv5M7IWV/OZbUfbeL++Qrzx1nVJwWROIFhCHRYQV4xbPNw=="
|
||||
"resolved" "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.6.tgz"
|
||||
"version" "7.18.6"
|
||||
|
@ -1294,7 +1294,7 @@
|
|||
"get-random-values" "^1.2.2"
|
||||
"lodash" "^4.17.15"
|
||||
|
||||
"@sanity/client@^3.2.0":
|
||||
"@sanity/client@^3.2.0", "@sanity/client@^3.3.2":
|
||||
"integrity" "sha512-M89v/KcNob0tnoMCW2caSnwYhSSqO1j5XuDXCloe966ZRFUAZWe9EVyyvvP7x1vPff5xR9PKxidrcx95l221mg=="
|
||||
"resolved" "https://registry.npmjs.org/@sanity/client/-/client-3.3.2.tgz"
|
||||
"version" "3.3.2"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
|
@ -14,22 +14,31 @@
|
|||
"format": "prettier --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.18.6",
|
||||
"@playwright/test": "^1.22.2",
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/kit": "next",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"eslint": "^8.16.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier-plugin-svelte": "^2.7.0",
|
||||
"svelte": "^3.44.0",
|
||||
"svelte-check": "^2.7.1",
|
||||
"svelte-preprocess": "^4.10.6",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tailwindcss": "^3.1.5",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.7.2",
|
||||
"vite": "^2.9.13"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sanity/client": "^3.3.2",
|
||||
"@sanity/image-url": "^1.0.1",
|
||||
"@sanity/types": "^2.29.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: SF Mono;
|
||||
src: url(SF-Mono-Light.otf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: SF Mono;
|
||||
font-weight: bold;
|
||||
src: url(SF-Mono-Semibold.otf);
|
||||
}
|
||||
|
||||
.sfmono {
|
||||
font-family: SF Mono, sans-serif;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export const getAvgRating = (ratings) => {
|
||||
if (!ratings) return 0;
|
||||
if (ratings.length === 1) return ratings[0].rating;
|
||||
else return ratings.reduce((prev, curr) => prev.rating + curr.rating) / ratings.length;
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
export * from './toProduct';
|
||||
export * from './parse';
|
||||
export * from './normalize';
|
||||
export * from './getAvgRating';
|
|
@ -0,0 +1,2 @@
|
|||
export const normalize = (str) => str.toLowerCase().trim()
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import {normalize }from './normalize';
|
||||
|
||||
export function parseSlug(slug) {
|
||||
return normalize(slug).replaceAll("-", " ")
|
||||
}
|
||||
|
||||
export function parseName(name) {
|
||||
return normalize(name).replaceAll(" ","-")
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import {normalize, parseName} from './index';
|
||||
|
||||
export const toProduct = (product, currentProduct) => {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('product', normalize(parseName(product.name)));
|
||||
window.history.pushState({}, '', url);
|
||||
currentProduct.set(product)
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import { urlFor } from '$lib/sanityClient';
|
||||
import { currentProduct } from '$lib/stores';
|
||||
import Rating from './Rating/Rating.svelte';
|
||||
import Tag from './Tag/Tag.svelte';
|
||||
console.log($currentProduct);
|
||||
|
||||
const { container, imageView, name, description, productInfo, date, tags, img, ratings } = {
|
||||
container: 'flex flex-col',
|
||||
imageView: 'flex m-16 gap-12',
|
||||
productInfo: 'flex flex-col gap-2',
|
||||
name: 'font-bold text-2xl',
|
||||
description: '',
|
||||
date: 'text-xs w-2/3 mt-auto mb-4',
|
||||
tags: 'flex gap-1',
|
||||
img: 'p-8 border border-black border-2 p-3',
|
||||
ratings: ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={container}>
|
||||
<div class={imageView}>
|
||||
{#if $currentProduct.image}
|
||||
<img
|
||||
src={urlFor($currentProduct.image).url()}
|
||||
class={img}
|
||||
width={300}
|
||||
alt={$currentProduct.name}
|
||||
/>
|
||||
{/if}
|
||||
<div class={productInfo}>
|
||||
<p class={name}>{$currentProduct.name}</p>
|
||||
{#if $currentProduct.tags}
|
||||
<div class={tags}>
|
||||
{#each $currentProduct.tags as tag (tag._ref)}
|
||||
<Tag {tag} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if $currentProduct.description}
|
||||
<p class={description}>{$currentProduct.description}</p>
|
||||
{/if}
|
||||
<p class={date}>Created on: {new Date($currentProduct._createdAt)}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{#if $currentProduct.rating}
|
||||
<div class={ratings}>
|
||||
{#each $currentProduct.rating as rating (rating._key)}
|
||||
<Rating {rating}/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
img {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
export let rating;
|
||||
import { emotions } from '$lib/stores';
|
||||
let foundEmotion;
|
||||
|
||||
if (rating.emotion) {
|
||||
foundEmotion = $emotions.find((emotion) => emotion._id === rating.emotion._ref).name;
|
||||
}
|
||||
|
||||
let stars = [];
|
||||
|
||||
for (let i = 0; i < rating.rating; i++) {
|
||||
stars.push('⭐');
|
||||
}
|
||||
|
||||
const { container, emotion, details, name, comments, ratingStars, spacer } = {
|
||||
container: 'border border-1 border-black h-full min-h-36 m-16 mt-0 mb-4 flex ',
|
||||
emotion: 'w-56 flex-shrink-0 bg-gray-100 flex flex-col items-center p-2 justify-between h-100',
|
||||
details: 'flex flex-col p-4',
|
||||
name: 'text-xl font-bold',
|
||||
comments: 'text-sm pt-2',
|
||||
ratingStars: 'mt-auto',
|
||||
spacer: 'h-8'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class={container}>
|
||||
{#if foundEmotion}
|
||||
<div class={emotion}>
|
||||
<img src="/" />
|
||||
{foundEmotion}
|
||||
</div>
|
||||
{/if}
|
||||
<div class={details}>
|
||||
<span class={name}>{rating.name} </span>
|
||||
{#if rating.comments}
|
||||
<p class={comments}>"{rating.comments}"</p>
|
||||
{/if}
|
||||
<div class={spacer} />
|
||||
<p class={ratingStars}>{stars.join('')}</p>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
export let tag;
|
||||
import { tags } from '$lib/stores';
|
||||
|
||||
const tagName = ($tags.find((dirTag) => dirTag._id === tag._ref)).name
|
||||
|
||||
const { container } = {
|
||||
container: 'text-sm flex rounded-md pl-2 pr-2 pt-1 pb-1 bg-gray-200'
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<p class={container}>{tagName}</p>
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import { urlFor } from './sanityClient';
|
||||
import { currentProduct } from '$lib/stores.js';
|
||||
import { toProduct } from '$helpers';
|
||||
export let products;
|
||||
|
||||
const {container} = {
|
||||
container: 'flex flex-wrap m-12 justify-start',
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class={container}>
|
||||
{#each products as product}
|
||||
<button on:click={() => toProduct(product, currentProduct)} >
|
||||
{#if product.image}
|
||||
<img src={urlFor(product.image).width(150).url()} alt={product.name} />
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
|
@ -0,0 +1,68 @@
|
|||
<script>
|
||||
import { productsView, products, tags } from '$lib/stores';
|
||||
import {normalize, getAvgRating } from '$helpers';
|
||||
export let filters;
|
||||
export let reset;
|
||||
let { selectedCat, selectedRating } = filters;
|
||||
|
||||
const stars = ['⭐ ⬆', '⭐⭐ ⬆', '⭐⭐⭐ ⬆', '⭐⭐⭐⭐ ⬆', '⭐⭐⭐⭐⭐ ⬆'];
|
||||
|
||||
const filterByCat = (products, value) =>
|
||||
products.filter((product) => {
|
||||
if (product.tags) {
|
||||
const selectedTagId = $tags.find((tag) => tag.name === value)._id;
|
||||
const productTags = product.tags.map((tag) => tag._ref);
|
||||
if (productTags.includes(selectedTagId)) return true;
|
||||
} else return false;
|
||||
});
|
||||
|
||||
const filterByRating = (products, value) =>
|
||||
products.filter((product) => product.rating && getAvgRating(product.rating) >= Number(value));
|
||||
|
||||
const filter = ({ target: { name, value } }) => {
|
||||
if (Number(value) === 0) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
switch (normalize(name)) {
|
||||
case 'rating':
|
||||
if (selectedCat)
|
||||
productsView.set(filterByCat(filterByRating($products, value), selectedCat));
|
||||
else productsView.set(filterByRating($products, value));
|
||||
break;
|
||||
case 'category':
|
||||
if (selectedRating)
|
||||
productsView.set(filterByRating(filterByCat($products, value), selectedRating));
|
||||
else productsView.set(filterByCat($products, value));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const { container, filterBar, filterTitle, filterSort } = {
|
||||
container: 'flex flex-col text-sm h-auto mb-4 mr-6',
|
||||
filterBar: 'pl-8 p-2 pr-2 flex justify-between',
|
||||
filterTitle: 'font-bold',
|
||||
filterSort: 'p-1 pl-9 mr-12 hover:bg-blue-300 w-full focus:outline-none'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class={container}>
|
||||
<div class={filterBar}>
|
||||
<div class={filterTitle}>Filter</div>
|
||||
<button on:click={reset}> ✖️ </button>
|
||||
</div>
|
||||
|
||||
<select on:change={filter} class={filterSort} name="category" bind:value={filters.selectedCat}>
|
||||
<option select="selected" value={0}>Category</option>
|
||||
{#each $tags as tag (tag.name)}
|
||||
<option value={tag.name}>{tag.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<select on:change={filter} class={filterSort} name="rating" bind:value={filters.selectedRating}>
|
||||
<option select="selected" value={0}>Rating</option>
|
||||
{#each stars as starValue, i (starValue)}
|
||||
<option value={i + 1}>{starValue}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<script>
|
||||
export let reset;
|
||||
const { title } = {
|
||||
title: 'hover:bg-blue-300 text-lg font-bold m-12 ml-0 pl-12'
|
||||
};
|
||||
</script>
|
||||
|
||||
<a href="/" class={title} on:click={reset}> Rating Room </a>
|
|
@ -0,0 +1,25 @@
|
|||
<script>
|
||||
export let productsView;
|
||||
export let currentProduct;
|
||||
import { toProduct } from '$helpers';
|
||||
|
||||
const { container, productStyle } = {
|
||||
container: 'pt-4',
|
||||
productList: 'flex flex-col items-start mt-10 text-sm',
|
||||
productStyle: 'w-full text-left'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class={container}>
|
||||
{#each productsView as product}
|
||||
<button class={productStyle} on:click={() => toProduct(product, currentProduct)}>
|
||||
<p
|
||||
class={`pl-12 hover:bg-blue-300 ${
|
||||
$currentProduct && $currentProduct.name === product.name ? 'bg-blue-300' : ''
|
||||
} text-sm`}
|
||||
>
|
||||
{product.name}
|
||||
</p>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
|
@ -0,0 +1,19 @@
|
|||
<script>
|
||||
import { products, productsView } from '$lib/stores';
|
||||
import { normalize } from '$helpers';
|
||||
|
||||
const searchProducts = (e) => {
|
||||
productsView.set(
|
||||
$products.filter((product) => normalize(product.name).includes(normalize(e.target.value)))
|
||||
);
|
||||
};
|
||||
|
||||
const { container, input } = {
|
||||
container: 'h-16 ml-auto mr-auto',
|
||||
input: 'outline outline-1 w-36 focus:outline-blue-600 p-1 text-sm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class={container}>
|
||||
<input on:input={searchProducts} class={input} placeholder="search" />
|
||||
</div>
|
|
@ -0,0 +1,51 @@
|
|||
<script>
|
||||
import SortOption from './SortOption.svelte';
|
||||
import { productsView } from '$lib/stores';
|
||||
import {normalize, getAvgRating } from '$helpers';
|
||||
|
||||
const sortOptions = ['Rating', 'Name', 'Created'];
|
||||
|
||||
const sort = (name, current) => {
|
||||
switch (normalize(name)) {
|
||||
case 'rating':
|
||||
productsView.set(
|
||||
$productsView.sort((prev, curr) => {
|
||||
if (current) return getAvgRating(prev.rating) < getAvgRating(curr.rating) ? -1 : 1;
|
||||
else return getAvgRating(prev.rating) < getAvgRating(curr.rating) ? 1 : -1;
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'name':
|
||||
productsView.set(
|
||||
$productsView.sort((prev, curr) => {
|
||||
if (current) return prev.name < curr.name ? -1 : 1;
|
||||
else return prev.name < curr.name ? 1 : -1;
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'created':
|
||||
productsView.set(
|
||||
$productsView.sort((prev, curr) => {
|
||||
if (current) return new Date(prev._createdAt) < new Date(curr._createdAt) ? -1 : 1;
|
||||
else return new Date(prev._createdAt) < new Date(curr._createdAt) ? 1 : -1;
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const { container, sortBar, sortTitle } = {
|
||||
container: 'flex flex-col text-sm h-auto mb-4 mr-6',
|
||||
sortBar: 'pl-8 p-2 flex justify-between',
|
||||
sortTitle: 'font-bold'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class={container}>
|
||||
<div class={sortBar}>
|
||||
<div class={sortTitle}>Sort</div>
|
||||
</div>
|
||||
{#each sortOptions as option}
|
||||
<SortOption {option} {sort} />
|
||||
{/each}
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
export let option;
|
||||
export let sort;
|
||||
let current = true;
|
||||
|
||||
const { sortOption, noPointer } = {
|
||||
sortOption: 'flex p-1 pl-9 mr-12 hover:bg-blue-300 w-full flex justify-between',
|
||||
noPointer: 'pointer-events-none'
|
||||
};
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={sortOption}
|
||||
on:click={(e) => {
|
||||
sort(e.target.name, current);
|
||||
current = !current;
|
||||
}}
|
||||
name={option}
|
||||
>
|
||||
<p class={noPointer}>{option}</p>
|
||||
<p class={noPointer}>{current ? '▲' : '▼'}</p>
|
||||
</button>
|
|
@ -0,0 +1,7 @@
|
|||
<script>
|
||||
const back = () => window.history.back()
|
||||
</script>
|
||||
|
||||
<div class='flex flex-col w-screen text-2xl p-24 items-start'><p>This item could not be found!</p>
|
||||
<button on:click={back} class='outline outline-1 p-1 mt-2'>Go back?</button>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
import SanityClientConstructor from '@sanity/client'
|
||||
import ImageUrlBuilder from '@sanity/image-url'
|
||||
import {api} from '../../backend/sanity.json'
|
||||
const {projectId, dataset} = api
|
||||
export const client = SanityClientConstructor({
|
||||
projectId,
|
||||
dataset,
|
||||
apiVersion: '2022-07-07',
|
||||
useCdn: true
|
||||
})
|
||||
const builder = ImageUrlBuilder(client)
|
||||
export function urlFor(source) {
|
||||
return builder.image(source)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
export const products = writable([])
|
||||
export const currentProduct = writable({})
|
||||
export const productsView = writable([])
|
||||
export const emotions = writable([])
|
||||
|
||||
export const tags = writable([])
|
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
import '../app.css';
|
||||
import { products, productsView, currentProduct } from '$lib/stores';
|
||||
import Products from '$lib/Layout/Products.svelte';
|
||||
import Header from '$lib/Layout/Header.svelte';
|
||||
import Search from '$lib/Layout/Search.svelte';
|
||||
import Filters from '$lib/Layout/Filters.svelte';
|
||||
import Sort from '$lib/Layout/Sort.svelte';
|
||||
let filters = { selectedCat: 0, selectedRating: 0 };
|
||||
|
||||
const reset = () => {
|
||||
productsView.set($products);
|
||||
const url = new URL(window.location);
|
||||
window.history.pushState({}, '', url);
|
||||
currentProduct.set({});
|
||||
filters = { selectedCat: 0, selectedRating: 0 };
|
||||
};
|
||||
|
||||
const { main, sidebar } = {
|
||||
main: 'flex w-screen h-screen sfmono',
|
||||
sidebar: 'flex flex-col justify-start h-screen overflow-auto w-52 shrink-0'
|
||||
};
|
||||
</script>
|
||||
|
||||
<main class={main}>
|
||||
<div class={sidebar}>
|
||||
<Header {reset} />
|
||||
{#if !Object.keys($currentProduct).length}
|
||||
<Search />
|
||||
<Filters bind:filters {reset} />
|
||||
<Sort />
|
||||
{:else}
|
||||
<Products productsView={$productsView} {currentProduct} />
|
||||
{/if}
|
||||
</div>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,25 @@
|
|||
import { client } from '$lib/sanityClient';
|
||||
/** @type {import('@sveltejs/kit').RequestHandler} */
|
||||
export async function get() {
|
||||
const products = await client.fetch('*[_type == "product"]');
|
||||
const tags = await client.fetch('*[_type == "tag"]');
|
||||
const emotions = await client.fetch('*[_type == "emotion"]');
|
||||
|
||||
|
||||
const data = {
|
||||
products,tags,emotions
|
||||
}
|
||||
|
||||
if (data) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
data: data
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 404
|
||||
};
|
||||
}
|
|
@ -1,2 +1,53 @@
|
|||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
||||
<script>
|
||||
import Grid from '$lib/Grid.svelte';
|
||||
import Feature from '$lib/Feature/Feature.svelte';
|
||||
import { products, productsView, tags, currentProduct, emotions } from '$lib/stores';
|
||||
import { browser } from '$app/env';
|
||||
import {normalize, parseSlug } from '$helpers';
|
||||
|
||||
export let data;
|
||||
products.set(data.products);
|
||||
tags.set(data.tags);
|
||||
emotions.set(data.emotions)
|
||||
|
||||
productsView.set(data.products);
|
||||
const goToProduct = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const paramProd = params.get('product');
|
||||
|
||||
if (paramProd) {
|
||||
const foundProduct = $products.find(
|
||||
({ name }) => normalize(name) === normalize(parseSlug(paramProd))
|
||||
);
|
||||
if (foundProduct) {
|
||||
currentProduct.set(foundProduct);
|
||||
}
|
||||
} else {
|
||||
const url = new URL(window.location.origin);
|
||||
history.pushState({}, '', url);
|
||||
currentProduct.set({});
|
||||
}
|
||||
};
|
||||
|
||||
if (browser) {
|
||||
window.onpopstate = () => {
|
||||
goToProduct();
|
||||
}
|
||||
}
|
||||
|
||||
const { container} = {
|
||||
container: 'h-screen overflow-auto'
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Rating Room</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class={container}>
|
||||
{#if Object.keys($currentProduct).length}
|
||||
<Feature />
|
||||
{:else}
|
||||
<Grid products={$productsView} />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -5,10 +5,12 @@ import preprocess from 'svelte-preprocess';
|
|||
const config = {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: preprocess(),
|
||||
preprocess: preprocess({
|
||||
postcss: true
|
||||
}),
|
||||
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
adapter: adapter(),
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
|
@ -1,13 +1,3 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { resolve } from 'path';
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
plugins: [sveltekit()]
|
||||
plugins: [sveltekit()],
|
||||
resolve: {
|
||||
alias: {
|
||||
$lib: resolve('./src/lib'),
|
||||
$helpers: resolve('./src/helpers')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['backend']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
Loading…
Reference in New Issue