diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6b998e2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}\\index.js" + } + ] +} diff --git a/api/items.js b/api/items.js index 4a3428d..90c2b4a 100644 --- a/api/items.js +++ b/api/items.js @@ -50,11 +50,6 @@ router.delete('/:uuid', async (req, res) => { }) router.post('/:uuid/images', upload.single('image'), async (req, res) => { - // TODO: Use the real user when we have authentication - req.user = { - uuid: '56881ad0-8d80-496a-b036-aed03d0895ce' - } - // Handle either image upload body or JSON body try { if(req.file) diff --git a/components/card/card.js b/components/card/card.js new file mode 100644 index 0000000..bf5ba3f --- /dev/null +++ b/components/card/card.js @@ -0,0 +1,36 @@ +import React from 'react' +import Link from 'next/link' + +import styles from './style.module.css' + +export default function Card({item, numberInCart}) { + let featuredImage = item.images.filter(i=>i.featured)[0] + if(!featuredImage) featuredImage = item.images[0] + + return ( +
+

{item.name}

+
+ {featuredImage && } +
+
{item.description}
+ +
+ ) +} diff --git a/components/card/style.module.css b/components/card/style.module.css new file mode 100644 index 0000000..b005e4e --- /dev/null +++ b/components/card/style.module.css @@ -0,0 +1,138 @@ +.card{ + background:white; + display:inline-block; + position:relative; + margin:20px 5%; + width:90%; + top:0; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); + transition: all 0.3s cubic-bezier(.25,.8,.25,1); +} + +.card:last-child { + /* Fixes the odd flickering bug */ + margin-bottom:0px; +} + +.card:hover { + box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); + top:-3px; +} + +.card h3{ + font-family:'Cormorant Infant', serif; + font-weight:600; + font-size:22px; + border-bottom:solid 1px black; + text-align:center; + margin:5px; +} + + +.card h3{ + border-bottom:none; +} +.card h3 a{ + color:inherit; + text-decoration:none; + border-bottom:solid 1px black; +} + +.card a:hover{ + color: #760c88; + border-color: #760c88; + transition: .2s ease-in-out; + transition-property: color, border-color; +} + +.card .card-text { + margin:0; + left: 40%; + top: 34px; +} + +.card .card-text p:first-child{ + margin-top:0px; +} + +.card .card-text{ + padding:10px; +} + +.card .card-text:after{ + content:''; + display:block; + clear:both; +} + +.card .card-image{ + width: 40%; + padding: 10px; + position:relative; + float:left; +} + +.card img{ + width:80%; + padding:10px; + background: #d4d4fb; + border: solid 1px #dec8e2; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); +} + +.card p, .card a{ + color:black; + opacity:.87; +} + +.card .card-details{ + font-family: 'Cormorant SC', serif; + list-style:none; + text-align:center; + margin-top:0px; + padding:0; +} + +.card .card-details.out-of-stock{ + opacity:.54; +} + +.card .card-details li a.disabled{ + opacity:.54; + text-decoration:none; +} + +.card .card-details.out-of-stock li.numberInStock{ + color:#5d0000 +} + +.card .card-details li{ + display:inline-block; + margin-right:20px; +} + +.card .card-details .numberInStock{ + font-weight:600; +} + +.card .card-details li:last-child{ + margin-right:0; +} +.catalogue{ + max-width:2000px; + column-count: 3; +} + +@media (max-width:400px){ + .card .card-image{ + width:100%; + float:none; + padding:0; + display: block; + margin: 10px auto; + text-align: center; + } + .card .card-image img{ + margin: 10px; + } +} diff --git a/db/mappings/item.js b/db/mappings/item.js index c58be3b..02035b6 100644 --- a/db/mappings/item.js +++ b/db/mappings/item.js @@ -19,7 +19,8 @@ module.exports = [{ 'description', 'urlslug', 'price_cents', - 'published' + 'published', + 'number_in_stock' ], collections: [ {name: 'images', mapId: 'imageMap', columnPrefix: 'image_'} @@ -42,4 +43,4 @@ module.exports = [{ collections: [ {name: 'items', mapId: 'itemMap', columnPrefix: 'item_'} ] -}] \ No newline at end of file +}] diff --git a/db/models/category.js b/db/models/category.js index 89424a0..b988666 100644 --- a/db/models/category.js +++ b/db/models/category.js @@ -6,7 +6,7 @@ const mappings = require('../mappings') const category = module.exports = {} category.findAll = async () => { - const query = 'select * from v_category' + const query = 'select * from sos.v_category' debug(query); @@ -16,7 +16,7 @@ category.findAll = async () => { category.create = async (name, urlslug, description) => { const query = { - text: 'select * from public.create_category($1::text, $2::citext, $3::text)', + text: 'select * from sos.create_category($1::text, $2::citext, $3::text)', values: [ name, urlslug, @@ -32,7 +32,7 @@ category.create = async (name, urlslug, description) => { category.addItem = async (category_uuid, item_uuid) => { const query = { - text: 'select * from public.add_item_to_category($1::uuid, $2::uuid)', + text: 'select * from sos.add_item_to_category($1::uuid, $2::uuid)', values: [ category_uuid, item_uuid @@ -47,7 +47,7 @@ category.addItem = async (category_uuid, item_uuid) => { category.removeItem = async (category_uuid, item_uuid) => { const query = { - text: 'select * from public.remove_item_from_category($1::uuid, $2::uuid)', + text: 'select * from sos.remove_item_from_category($1::uuid, $2::uuid)', values: [ category_uuid, item_uuid diff --git a/db/models/item.js b/db/models/item.js index 7277198..76369ee 100644 --- a/db/models/item.js +++ b/db/models/item.js @@ -8,7 +8,7 @@ const sharp = require('sharp') const item = module.exports = {} item.findAll = async () => { - const query = 'select * from v_item' + const query = 'select * from sos.v_item' debug(query); @@ -18,7 +18,7 @@ item.findAll = async () => { item.findById = async (item_uuid) => { const query = { - text: 'select * from v_item where item_uuid = $1', + text: 'select * from sos.v_item where item_uuid = $1', values: [ item_uuid ] @@ -32,7 +32,7 @@ item.findById = async (item_uuid) => { item.findBySlug = async (item_slug) => { const query = { - text: 'select * from v_item where item_urlslug = $1', + text: 'select * from sos.v_item where item_urlslug = $1', values: [ item_slug ] @@ -46,7 +46,7 @@ item.findBySlug = async (item_slug) => { item.create = async (name, urlslug, description, price_cents, published) => { const query = { - text: 'select * from public.create_item($1::text, $2::citext, $3::text, $4::integer, $5::boolean)', + text: 'select * from sos..create_item($1::text, $2::citext, $3::text, $4::integer, $5::boolean)', values: [ name, urlslug, @@ -75,7 +75,7 @@ item.addImage = async (item_uuid, image_buffer, uploader_uuid) => { ]) const query = { - text: 'select * from public.add_image_to_item($1, $2, $3, $4, $5)', + text: 'select * from sos.add_image_to_item($1, $2, $3, $4, $5)', values: [ item_uuid, image, @@ -90,8 +90,8 @@ item.addImage = async (item_uuid, image_buffer, uploader_uuid) => { } const imageSizeQueries = { - 'large': 'select * from get_image_large($1)', - 'thumb': 'select * from get_image_thumb($1)' + 'large': 'select * from sos.get_image_large($1)', + 'thumb': 'select * from sos.get_image_thumb($1)' } item.getImage = async (image_uuid, size) => { @@ -109,7 +109,7 @@ item.getImage = async (image_uuid, size) => { item.removeImage = async (image_uuid) => { const query = { - text: 'select * from public.delete_image($1)', + text: 'select * from sos.delete_image($1)', values: [ image_uuid ] @@ -121,7 +121,7 @@ item.removeImage = async (image_uuid) => { item.removeItem = async (item_uuid) => { const query = { - text: 'select * from public.delete_item($1)', + text: 'select * from sos.delete_item($1)', values: [ item_uuid ] diff --git a/db/models/session.js b/db/models/session.js index 2bb5778..ea216b8 100644 --- a/db/models/session.js +++ b/db/models/session.js @@ -7,7 +7,7 @@ const session = module.exports = {} session.create = async (user_uuid, ip_address, user_agent, referer, origin_link_uuid) => { const query = { - text: 'select * from login_user_session($1, $2, $3, $4, $5, $6)', + text: 'select * from sos.login_user_session($1, $2, $3, $4, $5, $6)', values: [ user_uuid, '2 hours', @@ -26,7 +26,7 @@ session.create = async (user_uuid, ip_address, user_agent, referer, origin_link_ session.validate = async (session_uuid) => { const query = { - text: 'select * from validate_session($1)', + text: 'select * from sos.validate_session($1)', values: [ session_uuid ] @@ -40,7 +40,7 @@ session.validate = async (session_uuid) => { session.update = async (session_uuid) => { const query = { - text: 'select * from update_session($1)', + text: 'select * from sos.update_session($1)', values: [ session_uuid ] diff --git a/db/models/user.js b/db/models/user.js index 7c4f862..13c61d4 100644 --- a/db/models/user.js +++ b/db/models/user.js @@ -41,7 +41,7 @@ user.register = async (email, password) => { const hash = await bcrypt.hash(password, saltRounds) const query = { - text: 'select * from register_user($1, $2)', + text: 'select * from sos.register_user($1, $2)', values: [ email, hash diff --git a/db/sql/0-setup.sql b/db/sql/0-setup.sql index 91f4da5..27414ba 100644 --- a/db/sql/0-setup.sql +++ b/db/sql/0-setup.sql @@ -1,7 +1,18 @@ CREATE database sos; +create schema sos; create user sos with encrypted password 'password'; + grant all privileges on database sos to sos; -GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO sos; +grant usage on schema public to sos; +grant usage on schema sos to sos; + +alter default privileges in schema public grant all privileges on all tables to sos; +alter default privileges in schema sos grant all privileges on all tables to sos; +alter default privileges in schema sos grant all privileges on all procedures to sos; + +grant all privileges on all tables in schema public to sos; +grant all privileges on all tables in schema sos to sos; create extension if not exists "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS citext; + diff --git a/db/sql/2-views.sql b/db/sql/2-views.sql index 713af81..41f90e9 100644 --- a/db/sql/2-views.sql +++ b/db/sql/2-views.sql @@ -1,4 +1,4 @@ -create or replace view public.v_session as +create or replace view sos.v_session as select "session".*, "session_user".user_email as session_user_email, @@ -8,35 +8,46 @@ create or replace view public.v_session as "session_user".user_time_email_confirmed as session_user_time_email_confirmed, "login_link".* from "session" - left join "user" "session_user" on "session".session_user_uuid = "session_user".user_uuid + left join "user" "session_user" on "session".session_user_uuid = "session_user".user_uuid left join "login_link" on "session".session_originating_link = "login_link".login_link_uuid; -create or replace view public.v_login_link as +create or replace view sos.v_login_link as select * from "login_link" left join "user" on "login_link".login_link_user_uuid = "user".user_uuid; -create or replace view public.v_cart as +create or replace view sos.v_cart as select * from "cart" left join "cart_item" on "cart".cart_uuid = "cart_item".cart_item_cart_uuid left join "item" on "cart_item".cart_item_item_uuid = "item".item_uuid; -create or replace view public.v_item as +create or replace view sos.v_item as select "item".*, "image".image_uuid, "image".image_featured, "image".image_mime_type, "image".image_date_uploaded, - "user".user_email + "user".user_email, + num_added - num_removed as item_number_in_stock from "item" - left join "image" on "item".item_uuid = "image".image_item_uuid - left join "user" on "image".image_uploader_uuid = "user".user_uuid; + left join "image" on item.item_uuid = image.image_item_uuid + left join "user" on image.image_uploader_uuid = "user".user_uuid + left join + ( + select + stockchange_item_uuid, + sum(case when stockchange_direction = 'added' then stockchange_change end)::int4 as num_added, + sum(case when stockchange_direction = 'subtracted' then stockchange_change end)::int4 as num_removed + from "item_stockchange" + group by stockchange_item_uuid + ) stock_counts + on stock_counts.stockchange_item_uuid = item.item_uuid; -create or replace view public.v_category as +create or replace view sos.v_category as select "category".*, "child_category".category_uuid as child_category_uuid, @@ -47,4 +58,4 @@ create or replace view public.v_category as left join "category_category" on "category".category_uuid = "category_category".category_category_parent_uuid left join "category" "child_category" on "category_category".category_category_child_uuid = "child_category".category_uuid left join "category_item" on "category".category_uuid = "category_item".category_item_category_uuid - left join v_item on "category_item".category_item_item_uuid = item_uuid; + left join sos.v_item on "category_item".category_item_item_uuid = item_uuid; diff --git a/db/sql/3-functions.sql b/db/sql/3-functions.sql index 2f900e0..152a9df 100644 --- a/db/sql/3-functions.sql +++ b/db/sql/3-functions.sql @@ -1,5 +1,5 @@ -create or replace function public.register_user(_email text, _password_hash text) - returns setof public.user +create or replace function sos.register_user(_email text, _password_hash text) + returns setof "user" language plpgsql as $function$ declare @@ -16,8 +16,8 @@ begin return query select * from "user" where user_uuid = _user_uuid; end; $function$; -create or replace function public.validate_session(_session_uuid uuid) - returns setof public.v_session +create or replace function sos.validate_session(_session_uuid uuid) + returns setof sos.v_session language plpgsql as $function$ begin @@ -26,8 +26,8 @@ begin and session_time_last_active + session_timeout_length > now(); end; $function$; -create or replace function public.update_session(_session_uuid uuid) - returns setof public.v_session +create or replace function sos.update_session(_session_uuid uuid) + returns setof sos.v_session language plpgsql as $function$ begin @@ -39,8 +39,8 @@ begin return query select * from validate_session(_session_uuid); end; $function$; -create or replace function public.login_user_session(_user_uuid uuid, _timeout_length interval, _ip_addr varchar(50), _user_agent varchar(500), _referer varchar(500), _link uuid) - returns setof public.v_session +create or replace function sos.login_user_session(_user_uuid uuid, _timeout_length interval, _ip_addr varchar(50), _user_agent varchar(500), _referer varchar(500), _link uuid) + returns setof sos.v_session language plpgsql as $function$ declare @@ -62,11 +62,11 @@ begin _link ) returning session_uuid into _session_uuid; - return query select * from public.validate_session(_session_uuid); + return query select * from sos.validate_session(_session_uuid); end; $function$; -create or replace function public.create_item(_name text, _urlslug citext, _description text, _price_cents integer, _published boolean) - returns setof public.v_item +create or replace function sos.create_item(_name text, _urlslug citext, _description text, _price_cents integer, _published boolean) + returns setof sos.v_item language plpgsql as $function$ declare @@ -86,11 +86,11 @@ begin _published ) returning item_uuid into _item_uuid; - return query select * from public.v_item where item_uuid = _item_uuid; + return query select * from sos.v_item where item_uuid = _item_uuid; end; $function$; -create or replace function public.add_image_to_item(_item_uuid uuid, _large_file bytea, _thumb_file bytea, _mime_type varchar, _uploader_uuid uuid) - returns setof public.v_item +create or replace function sos.add_image_to_item(_item_uuid uuid, _large_file bytea, _thumb_file bytea, _mime_type varchar, _uploader_uuid uuid) + returns setof sos.v_item language plpgsql as $function$ declare @@ -110,11 +110,11 @@ begin _uploader_uuid ) returning image_uuid into _image_uuid; - return query select * from public.v_item where item_uuid = _item_uuid; + return query select * from sos.v_item where item_uuid = _item_uuid; end; $function$; -create or replace function public.set_featured_image(_item_uuid uuid, _image_uuid uuid) - returns setof public.v_item +create or replace function sos.set_featured_image(_item_uuid uuid, _image_uuid uuid) + returns setof sos.v_item language plpgsql as $function$ begin @@ -127,10 +127,10 @@ begin image_featured = true where image_uuid = _image_uuid; - return query select * from public.v_item where item_uuid = _item_uuid; + return query select * from sos.v_item where item_uuid = _item_uuid; end; $function$; -create or replace function public.get_image_large(_image_uuid uuid) +create or replace function sos.get_image_large(_image_uuid uuid) returns table (image_uuid uuid, image_mime_type varchar, image_file bytea) language plpgsql as $function$ @@ -143,7 +143,7 @@ begin where "image".image_uuid = _image_uuid; end; $function$; -create or replace function public.get_image_thumb(_image_uuid uuid) +create or replace function sos.get_image_thumb(_image_uuid uuid) returns table (image_uuid uuid, image_mime_type varchar, image_file bytea) language plpgsql as $function$ @@ -156,8 +156,8 @@ begin where "image".image_uuid = _image_uuid; end; $function$; -create or replace function public.create_category(_category_name text, _category_urlslug citext, _category_description text) - returns setof public.v_category +create or replace function sos.create_category(_category_name text, _category_urlslug citext, _category_description text) + returns setof sos.v_category language plpgsql as $function$ declare diff --git a/pages/_app.js b/pages/_app.js index f541e87..48e4495 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -6,9 +6,19 @@ import Header from '~/components/header' import Footer from '~/components/footer' import "../styles/layout.css" -Layout.getInitialProps = async ({ctx}) => { - const {data: user} = await axios.get(`/api/auth`, { headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined }) - return {user} +Layout.getInitialProps = async ({Component, ctx}) => { + // Configure axios instance + ctx.axios = axios.create({ + headers: ctx.req ? {cookie: ctx.req.headers.cookie} : undefined + }) + + const {data: user} = await ctx.axios.get(`/api/auth`) + + let pageProps = {}; + if(Component.getInitialProps) + pageProps = await Component.getInitialProps({ctx}) + + return {pageProps, user} } function Layout({ Component, pageProps, user }){ diff --git a/pages/index.js b/pages/index.js index 4413e7e..f343ad2 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,11 +1,22 @@ import React from 'react' import Hero from '~/components/hero' +import Card from '~/components/card' -const Index = () => ( - <> - -

Homepage

- -) -export default Index +Index.getInitialProps = async ({ctx})=>{ + const {data: items} = await ctx.axios.get('/api/items') + return {items} +} + +export default function Index({items}){ + return ( + <> + +
+ {items.map(item=> + + )} +
+ + ) +} diff --git a/styles/layout.css b/styles/layout.css index 869a3f7..1de8307 100644 --- a/styles/layout.css +++ b/styles/layout.css @@ -20,3 +20,33 @@ main { color: black; padding: 30px 0; } + +.cardContainer{ + margin-left: auto; + margin-right: auto; + margin-bottom: 30px; + padding-left: 50px; + padding-right: 50px; + + max-width:2000px; + column-count: 3; +} + +@media (max-width:1200px){ + .cardContainer{ + padding:0 2%; + } +} + +@media (max-width: 1000px) { + .cardContainer { + column-count: 2; + } +} + +@media (max-width: 600px) { + .cardContainer { + margin-top: 20px; + column-count: 1; + } +}