-
-

-
Welcome to crates
-
- Try updating this message in the
- ./routes/index.tsx file, and recrates.
-
-
+
+
Welcome to crates
+
You are logged in!
);
}
diff --git a/crates/routes/login.tsx b/crates/routes/login.tsx
new file mode 100644
index 0000000..6c65bb8
--- /dev/null
+++ b/crates/routes/login.tsx
@@ -0,0 +1,125 @@
+import { h } from "preact";
+import { Handlers, PageProps } from "$fresh/server.ts";
+import { getCookies, setCookie } from "$std/http/cookie.ts";
+import Swirl from "../components/swirl.tsx";
+import { sessions } from "../api/auth.middleware.ts";
+import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts";
+import { createPg } from "../api/pg.ts";
+
+export const handler: Handlers = {
+ async GET(req, ctx) {
+ const cookies = getCookies(req.headers);
+ const sessionId = cookies.session;
+
+ // If user is already logged in, redirect to home
+ if (sessionId && sessions.has(sessionId)) {
+ return new Response("", {
+ status: 307,
+ headers: { Location: "/" },
+ });
+ }
+
+ return ctx.render();
+ },
+
+ async POST(req, ctx) {
+ const formData = await req.formData();
+ const email = formData.get("email")?.toString();
+ const password = formData.get("password")?.toString();
+
+ if (!email || !password) {
+ return ctx.render({ error: "Email and password required" });
+ }
+
+ try {
+ // Create pg instance for database operations
+ const pg = createPg(console);
+
+ // Find user by email
+ const user = await pg.userFindEmail(email);
+
+ if (!user) {
+ return ctx.render({ error: "Invalid credentials" });
+ }
+
+ // Verify password
+ const match = await bcrypt.compare(password, user.password);
+
+ if (!match) {
+ return ctx.render({ error: "Invalid credentials" });
+ }
+
+ // Create session
+ const sessionId = crypto.randomUUID();
+ const expiresAt = new Date();
+ expiresAt.setDate(expiresAt.getDate() + 7); // Session expires in 7 days
+
+ sessions.set(sessionId, {
+ userId: user.id,
+ expiresAt
+ });
+
+ // Set cookie and redirect
+ const headers = new Headers({ Location: "/" });
+ setCookie(headers, {
+ name: "session",
+ value: sessionId,
+ expires: expiresAt,
+ path: "/",
+ httpOnly: true,
+ secure: true,
+ sameSite: "Lax"
+ });
+
+ return new Response(null, {
+ status: 303,
+ headers
+ });
+ } catch (error) {
+ console.error("Login error:", error);
+ return ctx.render({ error: "An error occurred during login" });
+ }
+ }
+};
+
+function Login({ error }: { error?: string }) {
+ const errorEl = error
+ ?
{error}
+ : null;
+
+ return (
+
+ );
+}
+
+export default function LoginPage(props: PageProps) {
+ const { error } = props.data || {};
+
+ return (
+
+
+
+
+
record importing service
+
we bundle orders together from thousands of online stores to reduce the cost of shipping vinyl
+
then we get together and play them
+
+
+ );
+}
\ No newline at end of file
diff --git a/crates/routes/register.tsx b/crates/routes/register.tsx
new file mode 100644
index 0000000..9620cca
--- /dev/null
+++ b/crates/routes/register.tsx
@@ -0,0 +1,103 @@
+import { h } from "preact";
+import { Handlers, PageProps } from "$fresh/server.ts";
+import { getCookies } from "$std/http/cookie.ts";
+import { useState } from "preact/hooks";
+import Swirl from "../components/swirl.tsx";
+import { sessions } from "../api/auth.middleware.ts";
+
+export const handler: Handlers = {
+ async GET(req, ctx) {
+ const cookies = getCookies(req.headers);
+ const sessionId = cookies.session;
+
+ // If user is already logged in, redirect to home
+ if (sessionId && sessions.has(sessionId)) {
+ return new Response("", {
+ status: 307,
+ headers: { Location: "/" },
+ });
+ }
+
+ return ctx.render();
+ },
+};
+
+function Register() {
+ const [error, setError] = useState("");
+
+ async function handleSubmit(e) {
+ e.preventDefault();
+ setError("");
+
+ const email = e.target.email.value;
+ const password = e.target.password.value;
+ const confirm = e.target.confirm.value;
+
+ if (password !== confirm) {
+ setError("passwords do not match");
+ return;
+ }
+
+ try {
+ const response = await fetch("/api/auth/register", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email, password }),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || "Registration failed");
+ }
+
+ // Redirect on successful registration
+ window.location.href = "/";
+ } catch (err) {
+ setError(err.message);
+ }
+ }
+
+ const errorEl = error
+ ?
{error}
+ : null;
+
+ return (
+
+ );
+}
+
+export default function RegisterPage() {
+ return (
+
+
+
+
+
record importing service
+
we bundle orders together from thousands of online stores to reduce the cost of shipping vinyl
+
then we get together and play them
+
+
+ );
+}
\ No newline at end of file
diff --git a/crates/static/style/fonts.less b/crates/static/style/fonts.less
new file mode 100644
index 0000000..32c2f2e
--- /dev/null
+++ b/crates/static/style/fonts.less
@@ -0,0 +1,87 @@
+/* vietnamese */
+@font-face {
+ font-family: 'Barlow';
+ font-style: normal;
+ font-weight: 400;
+ src: url(https://fonts.gstatic.com/s/barlow/v12/7cHpv4kjgoGqM7E_A8s52Hs.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Barlow';
+ font-style: normal;
+ font-weight: 400;
+ src: url(https://fonts.gstatic.com/s/barlow/v12/7cHpv4kjgoGqM7E_Ass52Hs.woff2) format('woff2');
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Barlow';
+ font-style: normal;
+ font-weight: 400;
+ src: url(https://fonts.gstatic.com/s/barlow/v12/7cHpv4kjgoGqM7E_DMs5.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Noto Sans Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ09DdVXQQ.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Noto Sans Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ09ndVXQQ.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Noto Sans Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ09HdVXQQ.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Noto Sans Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ097dVXQQ.woff2) format('woff2');
+ unicode-range: U+0370-03FF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Noto Sans Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ09LdVXQQ.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Noto Sans Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ09PdVXQQ.woff2) format('woff2');
+ unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Noto Sans Mono';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ src: url(https://fonts.gstatic.com/s/notosansmono/v21/BngrUXNETWXI6LwhGYvaxZikqZqK6fBq6kPvUce2oAZcdthSBUsYck4-_FNJ093dVQ.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
\ No newline at end of file
diff --git a/crates/static/style/index.less b/crates/static/style/index.less
new file mode 100644
index 0000000..3b16d61
--- /dev/null
+++ b/crates/static/style/index.less
@@ -0,0 +1,194 @@
+// Digital Ocean reset
+html {
+ box-sizing: border-box;
+ font-size: 16px;
+}
+
+*, *:before, *:after {
+ box-sizing: inherit;
+}
+
+body, h4, h5, h6, p, ol, ul {
+ margin: 0;
+ padding: 0;
+ font-weight: normal;
+}
+
+ol, ul {
+ list-style: none;
+}
+
+img {
+ // max-width: 100%;
+ height: auto;
+}
+
+// stuff from UX laws
+html {
+ color:var(--base-font-color);
+ font-size:clamp(var(--min-font-size),calc(50% + 1vw),var(--max-font-size));
+ font-family:var(--base-font-family);
+ line-height:var(--base-line-height);
+ -webkit-text-size-adjust:100%;
+ background:var(--background-0);
+ box-sizing:border-box
+}
+
+// @charset "UTF-8";
+:root {
+ --breakpoint-medium: 35em;
+ --breakpoint-large: 56em;
+
+ --small-font-size: 0.75em;
+ --base-font-size: 1em;
+ --smedium-font-size: 1.333em;
+ --medium-font-size: 1.777em;
+ --large-font-size: 2.369em;
+ --xlarge-font-size: 3.157em;
+ --base-font-family: 'Barlow', Arial, Sans-Serif;
+ --secondary-font-family: 'Noto Sans Mono', monospace;
+ --base-font-color: var(--color-off-white);
+ --secondary-font-color: var(--color-mid-grey);
+
+ --base-line-height: 1.4;
+ --header-line-height: 1.2;
+
+ --base-border-color: var(--secondary-font-color);
+ --base-border: 2px solid var(--base-border-color);
+ --hover-border: 2px solid var(--base-font-color);
+ --focus-border: 2px solid var(--base-font-color);
+ --base-border-radius: .2rem;
+ --base-focus: 2px solid var(--base-accent-color);
+ --base-outline-offset: 0.25em;
+
+ --min-font-size: var(--base-font-size);
+ --max-font-size: var(--large-font-size);
+
+ --color-dark-blue: #101014;
+ --color-mid-blue: #16171d;
+ --color-light-blue: #1f2128;
+ --color-mid-grey: #9BA0B3;
+ --color-off-white: #F4F1D0;
+ --color-accent-blue: #5E9ED6;
+
+ --color-blue-gray: #506485;
+
+ --color-blue: #475977;
+ --color-green: #476d4a;
+ --color-red: #ae4442;
+
+ --color-accepted: var(--color-blue);
+ --color-confirmed: var(--color-green);
+
+ --base-accent-color: var(--color-accent-blue);
+ --fg-color: var(--color-off-white);
+ --hover-link-color: var(--color-off-white);
+
+ --background-0: var(--color-dark-blue);
+ --background-1: var(--color-mid-blue);
+ --background-2: var(--color-light-blue);
+
+ --spacing: 1rem;
+ --spacing-half: calc(var(--spacing)/2);
+ --spacing-double: calc(var(--spacing)*2);
+
+ --utility-bar-header-height: calc(var(--spacing) * 1.5);
+}
+
+button, input, optgroup, select, textarea {
+ margin:0;
+ border:var(--base-border);
+ border-radius:var(--base-border-radius);
+ background:0 0;
+ font-family:inherit;
+ font-size:100%;
+ color:inherit;
+ line-height:var(--base-line-height);
+}
+
+button, [type=submit] {
+ padding: var(--spacing-half);
+ background: var(--background-1);
+}
+
+input:hover, textarea:hover {
+ color:var(--base-font-color);
+ border:var(--hover-border)
+}
+
+input:focus, textarea:focus {
+ border:var(--focus-border);
+ outline:0
+}
+
+button, input {
+ overflow:visible;
+}
+
+button, select {
+ text-transform: none;
+}
+
+button, [type=button], [type=reset], [type=submit] {
+ -webkit-appearance:button;
+ border-color: transparent;
+ cursor:pointer;
+
+ font-family: var(--secondary-font-family);
+ font-size: var(--small-font-size);
+ // font-weight: bold;
+ text-transform: uppercase;
+
+ &:hover {
+ background: var(--background-2);
+ border-color: transparent;
+ }
+}
+
+h1, h2, h3, h4, h5, h6 {
+ margin-top:0;
+ line-height:var(--header-line-height);
+}
+
+h1 {
+ margin-bottom:var(--spacing);
+ font-size:var(--large-font-size)
+}
+
+h2 {
+ margin-bottom:var(--spacing);
+ font-size:var(--medium-font-size);
+}
+
+h3 {
+ margin-bottom:calc(var(--spacing)/2);
+ font-size:var(--smedium-font-size)
+}
+
+h4 {
+ margin-bottom:calc(var(--spacing)/3);
+ font-size:var(--base-font-size)
+}
+
+p {
+ margin:0 0 var(--spacing)
+}
+
+a {
+ color: var(--secondary-font-color);
+ font-family: var(--secondary-font-family);
+ text-transform: uppercase;
+
+ &:hover {
+ color: var(--fg-color);
+ }
+}
+
+.alt-text, .alt {
+ font-family: var(--secondary-font-family);
+ // font-size: var(--small-font-size);
+ text-transform: uppercase;
+}
+
+@import 'fonts.less';
+@import 'styles.less';
diff --git a/crates/static/style/styles.less b/crates/static/style/styles.less
new file mode 100644
index 0000000..5721297
--- /dev/null
+++ b/crates/static/style/styles.less
@@ -0,0 +1,422 @@
+body {
+ padding-top: calc(var(--spacing)/2);
+ overflow: hidden;
+}
+
+#mcrates {
+ padding: 0 var(--spacing-double) var(--spacing) var(--spacing-double);
+ display: grid;
+
+ grid-template-areas:
+ "header header"
+ "main main"
+ "footer footer";
+
+ height: 100vh;
+ width: 100vw;
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: min-content 1fr min-content;
+}
+
+.landing {
+ display: grid;
+
+ overflow: scroll;
+
+ grid-template-columns: 1fr 2fr;
+ grid-gap: calc(var(--spacing-double) * 2);
+ grid-template-areas:
+ "login swirl"
+ "login words";
+
+ .login {
+ grid-area: login;
+ button {
+ width: 100%;
+ }
+ }
+
+ .swirl {
+ grid-area: swirl;
+ height: 15rem;
+ }
+
+ .words {
+ grid-area: words;
+ text-align: center;
+ }
+}
+
+.info {
+ display: grid;
+
+ overflow: hidden;
+
+ grid-template-areas:
+ "words swirl"
+ "words contact";
+
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: min-content 1fr;
+
+ section {
+ margin-bottom: var(--spacing);
+ }
+
+ .contact {
+ grid-area: contact;
+ text-align: center;
+ overflow: scroll;
+ }
+
+ .words {
+ grid-area: words;
+ overflow: scroll;
+ }
+
+ .swirl {
+ grid-area: swirl;
+ height: 15rem;
+ }
+}
+
+article {
+ background-color: var(--background-1);
+
+ button {
+ border-color: var(--background-2);
+ }
+}
+
+.search {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+
+ overflow: hidden;
+
+ .results {
+ display: grid;
+ padding-left: var(--spacing);
+ grid-gap: var(--spacing);
+
+ article {
+ display: grid;
+ padding: var(--spacing);
+ grid-template-columns: 1fr 4fr min-content;
+ }
+
+ overflow: scroll;
+ }
+
+ button {
+ margin-right: var(--spacing);
+ background: var(--background-2);
+ border-color: var(--background-1);
+
+ &.added {
+ background: var(--color-green);
+ cursor: auto;
+ }
+ }
+}
+
+.list {
+ display: grid;
+ grid-template-columns:
+ 1fr 1fr 1fr;
+
+ grid-gap: var(--spacing);
+ overflow: scroll;
+
+ article {
+ display: grid;
+
+ grid-template-areas:
+ "title img"
+ "details details"
+ "remove remove";
+
+ button {
+ margin-top: var(--spacing-half);
+ grid-area: remove
+ }
+
+ .details {
+ grid-area: details;
+ }
+
+ padding: var(--spacing);
+
+ img {
+ justify-self: end;
+ }
+ }
+}
+
+form {
+ margin-bottom: var(--spacing);
+
+ label, input {
+ display: block;
+ }
+
+ input, button {
+ width: 100%;
+ }
+
+ input {
+ padding: var(--spacing-half);
+ margin-bottom: var(--spacing);
+ }
+
+ label {
+ font-family: var(--secondary-font-family);
+ font-size: var(--small-font-size);
+ text-transform: uppercase;
+ margin-bottom: var(--spacing-half);
+ }
+
+}
+
+header {
+ grid-area: header;
+
+ display: grid;
+ grid-template-columns: 1fr 1fr min-content;
+
+ border-bottom: var(--base-border);
+ padding-bottom: var(--spacing-half);
+ margin-bottom: var(--spacing-half);
+
+ a {
+ font-size: var(--small-font-size);
+ margin-right: var(--spacing);
+ }
+
+ a {
+ white-space: nowrap;
+ }
+}
+
+main {
+ grid-area: main;
+
+ display: grid;
+ grid-template-areas:
+ "nav content";
+
+ grid-template-columns:
+ 1fr 1fr;
+
+ column-gap: var(--spacing);
+
+ overflow: hidden;
+
+ nav {
+ // duck scrollbars
+ padding-right: var(--spacing-half);
+
+ grid-area: nav;
+
+ div {
+ display: grid;
+ grid-template-columns:
+ 1fr min-content min-content;
+
+ column-gap: var(--spacing-half);
+
+ a {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ overflow: scroll;
+ }
+
+ .offers {
+ grid-area: content;
+ overflow: scroll;
+ }
+}
+
+.acp {
+ overflow: scroll;
+
+ .bundles {
+ flex-flow: row;
+
+ .bundle {
+ margin-bottom: var(--spacing-double);
+
+ section {
+ margin-bottom: var(--spacing);
+ }
+
+ .items {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+
+ div {
+ margin-right: var(--spacing);
+ margin-bottom: var(--spacing);
+
+ display: grid;
+
+ &.bold {
+ font-weight: bold;
+ background: var(--background-2);
+ }
+ }
+ }
+ }
+ }
+}
+
+.offers {
+ display: grid;
+ // grid-template-columns: 1fr 1fr;
+ grid-gap: var(--spacing);
+
+ .offer {
+ display: grid;
+ grid-template-columns: 1fr min-content;
+ grid-template-rows: min-content min-content;
+ grid-template-areas:
+ "details img"
+ "lines lines";
+
+ padding: var(--spacing);
+ background: var(--background-1);
+
+ .details {
+ font-size: var(--small-font-size);
+ }
+
+ .status {
+ margin-top: var(--spacing-half);
+ }
+
+ section {
+ grid-area: details;
+ }
+
+ img {
+ grid-area: img;
+ justify-self: end;
+ margin-left: var(--spacing-half);
+ }
+
+ .lines {
+ grid-area: lines;
+ display: flex;
+ flex-wrap: wrap;
+ column-gap: var(--spacing-half);
+
+ button {
+ margin-top: var(--spacing-half);
+ border-color: var(--background-2);
+
+ &.accepted {
+ background: var(--color-accepted);
+ }
+
+ &.confirmed {
+ cursor: auto;
+ background: var(--color-confirmed);
+ }
+
+ &.neg {
+ margin-left: var(--spacing-half);
+ }
+ }
+ }
+ }
+}
+
+.number {
+ font-family: var(--secondary-font-family);
+ text-transform: uppercase;
+}
+
+.swirl {
+ background-image: url("/logo.svg");
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
+
+ margin-bottom: var(--spacing);
+}
+
+input[type='range']{ accent-color: var(--color-accepted); }
+
+@media (max-width:900px) {
+ .landing {
+ grid-template-columns: 1fr;
+ grid-template-areas:
+ "swirl"
+ "login"
+ "words";
+ }
+
+ .list {
+ grid-template-columns: 1fr;
+ }
+
+ .search {
+ grid-template-columns: 1fr;
+ }
+
+ .offers {
+ grid-template-columns: 1fr;
+ }
+
+ .info {
+ grid-template-columns: 1fr;
+ // is necessary for some reason
+ grid-template-areas:
+ "swirl"
+ "contact"
+ "words";
+ }
+
+ #mcrates {
+ padding: 0 var(--spacing) 0 var(--spacing);
+ }
+
+ main {
+ grid-template-columns: 1fr;
+ grid-template-rows: 1fr 3fr;
+ grid-gap: var(--spacing-half);
+ grid-template-areas:
+ "nav"
+ "content";
+ }
+}
+
+@media (max-width:1200px) {
+ img {
+ width: 5rem;
+ }
+}
+
+
+// ------------
+// toasts
+
+.iziToast {
+ border-radius: 0;
+ border-color: var(--base-border-color);
+
+ .iziToast-body {
+ font-family: var(--base-font-family);
+ font-size: var(--base-font-size);
+ }
+
+ .iziToast-title {
+ font-family: var(--secondary-font-family);
+ text-transform: uppercase;
+ }
+}
\ No newline at end of file