Merge pull request #12 from zschaffer/dev

Initial Prototype
This commit is contained in:
Xinrui Chen 2022-07-17 19:09:39 -07:00 committed by GitHub
commit c4b0608c4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 5956 additions and 336 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ node_modules
/build
/.svelte-kit
/package
/backend/dist
.env
.env.*
!.env.example

4
CONTRIBUTERS.md Normal file
View File

@ -0,0 +1,4 @@
## Collaborators
- [Xinrui Chen](github.com/xynree)
- [Zane Schaffer](github.com/zschaffer)

View File

@ -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.

View File

@ -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>

View File

@ -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

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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'
}
}
};

109
backend/schemas/products.js Normal file
View File

@ -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,
]
};

View File

@ -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! */
]),
})

18
backend/schemas/tags.js Normal file
View File

@ -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',
}
]
}

View File

@ -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"

5217
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

6
postcss.config.cjs Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

19
src/app.css Normal file
View File

@ -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;
}

View File

@ -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;
};

4
src/helpers/index.js Normal file
View File

@ -0,0 +1,4 @@
export * from './toProduct';
export * from './parse';
export * from './normalize';
export * from './getAvgRating';

2
src/helpers/normalize.js Normal file
View File

@ -0,0 +1,2 @@
export const normalize = (str) => str.toLowerCase().trim()

9
src/helpers/parse.js Normal file
View File

@ -0,0 +1,9 @@
import {normalize }from './normalize';
export function parseSlug(slug) {
return normalize(slug).replaceAll("-", " ")
}
export function parseName(name) {
return normalize(name).replaceAll(" ","-")
}

8
src/helpers/toProduct.js Normal file
View File

@ -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)
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

21
src/lib/Grid.svelte Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

7
src/lib/NotFound.svelte Normal file
View File

@ -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>

14
src/lib/sanityClient.js Normal file
View File

@ -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)
}

8
src/lib/stores.js Normal file
View File

@ -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([])

View File

@ -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>

25
src/routes/index.js Normal file
View File

@ -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
};
}

View File

@ -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>

BIN
static/SF-Mono-Light.otf Executable file

Binary file not shown.

BIN
static/SF-Mono-Semibold.otf Executable file

Binary file not shown.

View File

@ -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(),
}
};

7
tailwind.config.cjs Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -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
}
}

View File

@ -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;