diff --git a/README.md b/README.md index 31c71df..bbe1836 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,52 @@ # Awesome Radio +Awesome Radio is a personal internet radio station aggregator. + +![Screenshot](screenshot.png) + +## Features + +* Browse radio stations by tag +* Listen while navigating +* Fully responsive UI for mobile, tablets, and desktop +* Light and dark theme automatically enabled by OS settings +* Deep linking for all UI actions +* User accounts +* Add content sources to import stations + +## Roadmap + +* Support user favorites +* Support importing from other source types +* Support manually adding/editing/disabling stations +* Support [PWA](https://web.dev/progressive-web-apps/) to allow user to save to home screen on mobile devices +* Fix: Primary drawer stays open after navigation +* Tech Debt: Add more unit and E2E tests + ## Development +### Tech Stack + +* [Remix](https://remix.run): React SSR web framework +* [SQLite](https://www.sqlite.org): File based relational database +* [Prisma](https://www.prisma.io/): Node TS ORM +* [Vitest](https://vitest.dev): Unit test framework + +### Getting Started + +1. Create `.env` file from `.env.example` + +```shell +cp .env.example .env +``` + +2. Migrate & Seed the SQLite DB + +```shell +npx prisma migrate deploy +npx prisma db seed +``` + ### Running ```shell @@ -10,6 +55,20 @@ npm run dev ### Testing +Run unit tests + ```shell npm run test ``` + +Run E2E tests + +```shell +npm run test:e2e:run +``` + +Run all checks + +```shell +npm run validate +``` diff --git a/app/root.tsx b/app/root.tsx index 641e3b7..a83a25c 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,6 +1,5 @@ import { cssBundleHref } from "@remix-run/css-bundle"; -import type { LinksFunction } from "@remix-run/node"; -import { V2_MetaFunction } from "@remix-run/node"; +import type { LinksFunction, V2_MetaFunction } from "@remix-run/node"; import { isRouteErrorResponse, Links, diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 7ad7f79..9212fd0 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,16 +1,5 @@ -export default function Index() { - return ( -
-
-
-

Hello there

-

Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi - exercitationem quasi. In deleniti eaque aut repudiandae et a id nisi.

- -
-
-
- ); +import { redirect } from "@remix-run/node"; + +export function loader() { + return redirect("/listen"); } diff --git a/app/routes/healthcheck.tsx b/app/routes/healthcheck.tsx index 6c520ce..0658658 100644 --- a/app/routes/healthcheck.tsx +++ b/app/routes/healthcheck.tsx @@ -4,22 +4,22 @@ import type { LoaderArgs } from "@remix-run/node"; import { prisma } from "~/db.server"; export const loader = async ({ request }: LoaderArgs) => { - const host = - request.headers.get("X-Forwarded-Host") ?? request.headers.get("host"); + const host = + request.headers.get("X-Forwarded-Host") ?? request.headers.get("host"); - try { - const url = new URL("/", `http://${host}`); - // if we can connect to the database and make a simple query - // and make a HEAD request to ourselves, then we're good. - await Promise.all([ - prisma.user.count(), - fetch(url.toString(), { method: "HEAD" }).then((r) => { - if (!r.ok) return Promise.reject(r); - }), - ]); - return new Response("OK"); - } catch (error: unknown) { - console.log("healthcheck ❌", { error }); - return new Response("ERROR", { status: 500 }); - } + try { + const url = new URL("/", `http://${host}`); + // if we can connect to the database and make a simple query + // and make a HEAD request to ourselves, then we're good. + await Promise.all([ + prisma.user.count(), + fetch(url.toString(), { method: "HEAD" }).then((r) => { + if (!r.ok) return Promise.reject(r); + }) + ]); + return new Response("OK"); + } catch (error: unknown) { + console.log("healthcheck ❌", { error }); + return new Response("ERROR", { status: 500 }); + } }; diff --git a/app/utils.test.ts b/app/utils.test.ts index 7ffd94a..a51cdf4 100644 --- a/app/utils.test.ts +++ b/app/utils.test.ts @@ -1,13 +1,38 @@ -import { validateEmail } from "./utils"; +import { describe, expect, it } from "vitest"; +import { slugify, validateEmail } from "./utils"; -test("validateEmail returns false for non-emails", () => { - expect(validateEmail(undefined)).toBe(false); - expect(validateEmail(null)).toBe(false); - expect(validateEmail("")).toBe(false); - expect(validateEmail("not-an-email")).toBe(false); - expect(validateEmail("n@")).toBe(false); + +describe("utils", () => { + describe("validateEmail", () => { + it("should returns false for non-emails", () => { + expect(validateEmail(undefined)).toBe(false); + expect(validateEmail(null)).toBe(false); + expect(validateEmail("")).toBe(false); + expect(validateEmail("not-an-email")).toBe(false); + expect(validateEmail("n@")).toBe(false); + }); + + it("should returns true for emails", () => { + expect(validateEmail("kody@example.com")).toBe(true); + }); + }); + + describe("slugify", () => { + it("should convert text into url safe text", () => { + expect(slugify("Abc dEf")).toBe("abc-def"); + expect(slugify(" abc def ")).toBe("abc-def"); + expect(slugify("abcDef")).toBe("abcdef"); + expect(slugify("abc.def")).toBe("abcdef"); + expect(slugify("abc!def")).toBe("abcdef"); + expect(slugify("abcdëf")).toBe("abcdef"); + expect(slugify("abc--def")).toBe("abc-def"); + expect(slugify("abc&def")).toBe("abc-and-def"); + expect(slugify("abc12def")).toBe("abc12def"); + expect(slugify("abc_def")).toBe("abc-def"); + }); + }); }); -test("validateEmail returns true for emails", () => { - expect(validateEmail("kody@example.com")).toBe(true); -}); + + + diff --git a/app/utils.ts b/app/utils.ts index 323460d..3908ba0 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -77,13 +77,6 @@ export function notFound(message?: string) { }); } -export function createIndex(records: T[], keyFn: (t: T) => string): Map { - return records.reduce((index, record) => { - index.set(keyFn(record), record); - return index; - }, new Map()); -} - export function slugify(string: string): string { const a = "àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;"; const b = "aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------"; diff --git a/cypress/e2e/smoke.cy.ts b/cypress/e2e/smoke.cy.ts index 153a99e..2c21339 100644 --- a/cypress/e2e/smoke.cy.ts +++ b/cypress/e2e/smoke.cy.ts @@ -15,7 +15,7 @@ describe("smoke tests", () => { cy.visitAndCheck("/"); - cy.findByRole("link", { name: /sign up/i }).click(); + cy.findByRole("link", { name: /join/i }).click(); cy.findByRole("textbox", { name: /email/i }).type(loginForm.email); cy.findByLabelText(/password/i).type(loginForm.password); diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..b7a47dc Binary files /dev/null and b/screenshot.png differ