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.
+
+
+
+## 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