= [
10 | {
11 | id: 1,
12 | status: Status.Published,
13 | date_created: "2023-01-01T12:00:00",
14 | date_updated: null,
15 | author: user.id,
16 | slug: "remix-vs-next",
17 | title: "Remix vs Next.js",
18 | content: `TL;DR Remix is as fast or faster than Next.js at serving static content Remix is faster than Next.js at serving dynamic content Remix enables fast user experiences even on slow networks Remix automatically handles errors, interruptions, and race conditions, Next.js doesn't Next.js encourages client side JavaScript for serving dynamic content, Remix doesn't Next.js requires client side JavaScript for data mutations, Remix doesn't Next.js build times increase linearly with your data, Remix build times are nearly instant and decoupled from data Next.js requires you to change your application architecture and sacrifice performance when your data scales We think Remix's abstractions lead to better application code Read full article
`
19 | },
20 | {
21 | id: 2,
22 | status: Status.Published,
23 | date_created: "2023-01-01T12:00:00",
24 | date_updated: null,
25 | author: user.id,
26 | slug: "everything-you-need-to-know-about-directus-roles",
27 | title: "Everything you need to know about Directus Roles",
28 | content: `In Directus, there is a User Management module where you can add new users to your platform. By default, Directus only has 2 roles available for these users, Public (no access) and Adminisrator (full access). In this article I will cover how you can create and use Roles to build a successful team and secure your application.
Roles are a core feature of Directus that controls how your users interact with the data. It can be as simple as read and write access or as complex as specific fields within a collection. It's good practice to create a new role for each new user or team and limit their access to what they need. You can always grant access when something else is required.
Read full article
`
29 | },
30 | {
31 | id: 3,
32 | status: Status.Published,
33 | date_created: "2023-01-01T12:00:00",
34 | date_updated: null,
35 | author: user.id,
36 | slug: "how-to-backup-directus",
37 | title: "How to backup Directus",
38 | content: `To backup Directus, make a copy of your project files, .env file and perform a data dump of your database.
Backing-up a Project It is important to make a backup of your project in case everything goes very badly. Some updates will make changes to your database which will making rolling back to previous versions impossible.
Make a copy of the files within each storage adapter , and store them in a safe place. This can be a zip/tar file somewhere on the same server so you can easily extract them again if needed. If space is an issue, the backup can be stored elsewhere once you are happy the update has succeeded. Make a copy of the Env file (/project_folder/.env
), and store it in a safe place. This contains all your configurations. Create a database dump (a sql backup of your database) Read full article
`
39 | }
40 | ];
41 |
42 | await knex(Collection.Articles).insert(articles);
43 | }
44 |
--------------------------------------------------------------------------------
/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cool-stack/configs/tsconfig/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "allowJs": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
8 | "strict": true,
9 | "target": "ES2019",
10 | "esModuleInterop": true,
11 | "isolatedModules": true,
12 | "moduleResolution": "node",
13 | "noEmit": true,
14 | "resolveJsonModule": true,
15 | },
16 | "include": ["extensions/**/*.ts", "seeds/**/*.ts", "types/**/*.ts"],
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/api/types/Articles.ts:
--------------------------------------------------------------------------------
1 | import type {User} from "./Users";
2 | import {isStatus, Status} from "./Status";
3 |
4 | export interface Article {
5 | id: number;
6 | status: Status;
7 | date_created: string;
8 | date_updated: null | string;
9 | author: string | User;
10 | slug: string;
11 | title: string;
12 | content: string;
13 | }
14 |
15 | export function isArticle(article: any): article is Article {
16 | return !!(
17 | article &&
18 | typeof article === "object" &&
19 | typeof article.id === "number" &&
20 | isStatus(article.status) &&
21 | typeof article.date_created === "string" &&
22 | (typeof article.date_updated === "object" || typeof article.date_updated === "string") &&
23 | typeof article.slug === "string" &&
24 | typeof article.title === "string" &&
25 | typeof article.content === "string" &&
26 | //relational fields
27 | "author" in article
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/api/types/Status.ts:
--------------------------------------------------------------------------------
1 | export enum Status {
2 | Draft = "draft",
3 | Published = "published",
4 | Archived = "archived"
5 | }
6 |
7 | export function isStatus(status: any): status is Status {
8 | return !!(
9 | status &&
10 | typeof status === "string" &&
11 | Object.values(Status).includes(status as Status)
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/api/types/Users.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id: string;
3 | email: string;
4 | }
5 |
6 | export function isUser(user: any): user is User {
7 | return !!(
8 | user &&
9 | typeof user === "object" &&
10 | typeof user.id === "string" &&
11 | typeof user.email === "string"
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/api/types/index.ts:
--------------------------------------------------------------------------------
1 | import type {Article} from "./Articles";
2 | import type {User} from "./Users";
3 |
4 | export * from "./Articles";
5 | export * from "./Users";
6 | export * from "./Status";
7 |
8 | export enum Collection {
9 | Articles = "articles",
10 | Users = "directus_users"
11 | }
12 |
13 | export type Collections = {
14 | [Collection.Articles]: Article;
15 | [Collection.Users]: User;
16 | };
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cool-stack",
3 | "description": "Ice-cool 🧊 Remix + Directus starter template.",
4 | "version": "1.0.0",
5 | "repository": "https://github.com/tdsoftpl/cool-stack.git",
6 | "private": true,
7 | "license": "MIT",
8 | "workspaces": [
9 | "api",
10 | "packages/*",
11 | "page"
12 | ],
13 | "scripts": {
14 | "bootstrap": "yarn && turbo run generate:css && turbo run build",
15 | "dev": "turbo run dev --parallel",
16 | "build": "turbo run build",
17 | "clean": "turbo run clean",
18 | "lint": "turbo run lint",
19 | "format": "prettier --write \"**/*.{js,ts,tsx,md}\"",
20 | "test": "turbo run test --filter=*/page --filter=*/ui --no-cache --only",
21 | "graph": "turbo run build --graph"
22 | },
23 | "devDependencies": {
24 | "@tdsoft/prettier-config": "^1.0.2",
25 | "prettier": "2.8.8",
26 | "turbo": "latest"
27 | },
28 | "resolutions": {
29 | "@types/react": "18.3.5",
30 | "@types/react-dom": "18.3.0",
31 | "react": "18.3.1",
32 | "react-dom": "18.3.1"
33 | },
34 | "engines": {
35 | "npm": ">=7.0.0",
36 | "node": ">=14"
37 | },
38 | "packageManager": "yarn@1.22.22"
39 | }
40 |
--------------------------------------------------------------------------------
/packages/configs/jest/jest-common.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: "ts-jest",
3 | resetMocks: true,
4 | passWithNoTests: true,
5 | testEnvironment: "jsdom",
6 | setupFilesAfterEnv: ["@testing-library/jest-dom"],
7 | transform: {
8 | "\\.m?js$": "babel-jest"
9 | },
10 | moduleDirectories: ["node_modules"],
11 | moduleFileExtensions: ["js", "jsx", "json", "ts", "tsx"]
12 | };
13 |
--------------------------------------------------------------------------------
/packages/configs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cool-stack/configs",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "files": [
6 | "./tsconfig/base.json",
7 | "./tsconfig/react-library.json",
8 | "./jest/jest-common.js",
9 | "./tailwind/tailwind.config.js"
10 | ],
11 | "peerDependencies": {
12 | "@tailwindcss/typography": "^0.5.9",
13 | "tailwindcss": "^3.2.4"
14 | },
15 | "devDependencies": {
16 | "@tailwindcss/typography": "0.5.15",
17 | "tailwindcss": "3.4.10"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/configs/tailwind/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const tailwindTypography = require("@tailwindcss/typography");
2 |
3 | module.exports = {
4 | theme: {},
5 | plugins: [tailwindTypography]
6 | };
7 |
--------------------------------------------------------------------------------
/packages/configs/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": true,
7 | "declarationMap": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": true,
12 | "moduleResolution": "node",
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "preserveWatchOutput": true,
16 | "skipLibCheck": true,
17 | "strict": true
18 | },
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/configs/tsconfig/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "lib": ["ES2015", "dom"],
7 | "module": "ESNext",
8 | "target": "ES6",
9 | "jsx": "react-jsx"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/ui/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ["@typescript-eslint", "prettier"],
3 | extends: [
4 | "eslint:recommended",
5 | "plugin:prettier/recommended",
6 | "plugin:import/recommended",
7 | "plugin:unicorn/recommended",
8 | "plugin:jest-dom/recommended",
9 | "plugin:react/recommended",
10 | "plugin:react-hooks/recommended"
11 | ],
12 | env: {
13 | browser: true,
14 | node: true,
15 | es2022: true,
16 | jest: true
17 | },
18 | ignorePatterns: ["**/*.js"],
19 | parser: "@typescript-eslint/parser",
20 | parserOptions: {
21 | project: "tsconfig.json",
22 | tsconfigRootDir: __dirname,
23 | sourceType: "module"
24 | },
25 | settings: {
26 | react: {
27 | version: "detect"
28 | },
29 | "import/resolver": {
30 | node: {
31 | extensions: [".ts", ".tsx"],
32 | moduleDirectory: ["src", "node_modules"]
33 | }
34 | }
35 | },
36 | rules: {
37 | "no-unused-vars": "off",
38 | "@typescript-eslint/no-unused-vars": "warn",
39 | "unicorn/text-encoding-identifier-case": "off",
40 | "unicorn/filename-case": "off",
41 | "unicorn/no-null": "off",
42 | "unicorn/no-nested-ternary": "off",
43 | "unicorn/no-array-callback-reference": "off",
44 | "unicorn/prevent-abbreviations": [
45 | "warn",
46 | {
47 | replacements: {
48 | args: false,
49 | props: false,
50 | ref: false
51 | }
52 | }
53 | ]
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/packages/ui/.gitignore:
--------------------------------------------------------------------------------
1 | src/style.css
2 |
--------------------------------------------------------------------------------
/packages/ui/.prettierrc:
--------------------------------------------------------------------------------
1 | "@tdsoft/prettier-config"
2 |
--------------------------------------------------------------------------------
/packages/ui/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
3 | addons: [
4 | "@storybook/addon-links",
5 | "@storybook/addon-essentials",
6 | "@storybook/addon-viewport",
7 | "@storybook/addon-interactions",
8 | "@storybook/addon-a11y",
9 | "@storybook/addon-storysource",
10 | "storybook-addon-designs",
11 | {
12 | name: "@storybook/addon-postcss",
13 | options: {
14 | postcssLoaderOptions: {
15 | implementation: require("postcss")
16 | }
17 | }
18 | }
19 | ],
20 | framework: "@storybook/react"
21 | };
22 |
--------------------------------------------------------------------------------
/packages/ui/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import "../src/style.css";
2 |
3 | export const parameters = {
4 | actions: {argTypesRegex: "^on[A-Z].*"},
5 | controls: {
6 | matchers: {
7 | color: /(background|color)$/i,
8 | date: /Date$/
9 | }
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/packages/ui/babel.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Babel is used only in the test environment for transforming packages from esm to cjs.
3 | */
4 | module.exports = {
5 | env: {
6 | test: {
7 | plugins: ["@babel/plugin-transform-modules-commonjs"]
8 | }
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/packages/ui/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ...require("@cool-stack/configs/jest/jest-common"),
3 | rootDir: "./",
4 | /**
5 | * Jest doesn't transform node_modules by default,
6 | * so one needs to modify transformIgnorePatterns
7 | * and include all modules to be transformed from ESM to CJS.
8 | */
9 | transformIgnorePatterns: []
10 | };
11 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cool-stack/ui",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "main": "./src/index.ts",
7 | "types": "./src/index.ts",
8 | "scripts": {
9 | "dev": "concurrently \"yarn run dev:storybook\" \"yarn run dev:css\"",
10 | "dev:storybook": "start-storybook --quiet -p 6006",
11 | "dev:css": "yarn generate:css --watch",
12 | "build:storybook": "build-storybook",
13 | "generate:css": "tailwindcss -i ./src/global.css -o ./src/style.css",
14 | "lint": "cross-env TIMING=1 eslint 'src/**/*.{js,jsx,ts,tsx}'",
15 | "test": "jest"
16 | },
17 | "peerDependencies": {
18 | "clsx": "^1.2.1",
19 | "react": "18.3.1"
20 | },
21 | "dependencies": {
22 | "clsx": "1.2.1",
23 | "react": "18.3.0"
24 | },
25 | "devDependencies": {
26 | "@babel/core": "7.25.2",
27 | "@babel/plugin-transform-modules-commonjs": "7.24.8",
28 | "@cool-stack/configs": "*",
29 | "@storybook/addon-a11y": "6.5.16",
30 | "@storybook/addon-actions": "6.5.16",
31 | "@storybook/addon-essentials": "6.5.16",
32 | "@storybook/addon-interactions": "6.5.16",
33 | "@storybook/addon-links": "6.5.16",
34 | "@storybook/addon-postcss": "2.0.0",
35 | "@storybook/addon-storysource": "6.5.16",
36 | "@storybook/addon-viewport": "6.5.16",
37 | "@storybook/react": "6.5.16",
38 | "@testing-library/jest-dom": "5.17.0",
39 | "@testing-library/react": "13.4.0",
40 | "@types/jest": "29.5.12",
41 | "@types/react": "18.2.79",
42 | "@typescript-eslint/eslint-plugin": "5.62.0",
43 | "@typescript-eslint/parser": "5.62.0",
44 | "babel-jest": "29.7.0",
45 | "babel-loader": "9.1.3",
46 | "concurrently": "7.6.0",
47 | "eslint": "8.57.0",
48 | "eslint-config-prettier": "8.10.0",
49 | "eslint-plugin-import": "2.30.0",
50 | "eslint-plugin-jest-dom": "4.0.3",
51 | "eslint-plugin-prettier": "4.2.1",
52 | "eslint-plugin-react": "7.35.2",
53 | "eslint-plugin-react-hooks": "4.6.2",
54 | "eslint-plugin-unicorn": "45.0.2",
55 | "jest": "29.7.0",
56 | "jest-environment-jsdom": "29.7.0",
57 | "postcss": "8.4.38",
58 | "prettier": "2.8.8",
59 | "prettier-plugin-tailwindcss": "0.6.6",
60 | "storybook-addon-designs": "6.3.1",
61 | "tailwindcss": "3.4.10",
62 | "ts-jest": "29.2.5",
63 | "typescript": "4.9.5"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/packages/ui/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/packages/ui/src/Avatar/Avatar.mocks.ts:
--------------------------------------------------------------------------------
1 | import {AvatarProps} from "./Avatar.props";
2 |
3 | export const avatarPropsMock: AvatarProps = {
4 | src: "https://avatars.githubusercontent.com/u/42771191?s=200&v=4",
5 | nickname: "John Doe"
6 | };
7 |
--------------------------------------------------------------------------------
/packages/ui/src/Avatar/Avatar.props.ts:
--------------------------------------------------------------------------------
1 | export interface AvatarProps {
2 | nickname: string;
3 | src?: string;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/ui/src/Avatar/Avatar.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type {ComponentStory, ComponentMeta} from "@storybook/react";
3 | import Avatar from "./Avatar";
4 | import {avatarPropsMock} from "./Avatar.mocks";
5 |
6 | export default {
7 | title: "Avatar",
8 | component: Avatar
9 | } as ComponentMeta;
10 |
11 | const Template: ComponentStory = (args) => ;
12 |
13 | export const Default = Template.bind({});
14 | Default.args = avatarPropsMock;
15 |
16 | export const NoImageSource = Template.bind({});
17 | NoImageSource.args = {...avatarPropsMock, src: undefined};
18 |
--------------------------------------------------------------------------------
/packages/ui/src/Avatar/Avatar.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {screen, render} from "@testing-library/react";
3 |
4 | import Avatar from "./Avatar";
5 | import {avatarPropsMock} from "./Avatar.mocks";
6 |
7 | describe(Avatar.name, () => {
8 | it("should render the user's initials", () => {
9 | render( );
10 | expect(screen.getByText("JD")).toBeInTheDocument();
11 | });
12 |
13 | it("should render provided avatar image", () => {
14 | const {container} = render( );
15 | const image = container.querySelector("img");
16 | expect(image).toBeInTheDocument();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/packages/ui/src/Avatar/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {AvatarProps} from "./Avatar.props";
3 | import {generateInitials} from "./Avatar.utils";
4 |
5 | const Avatar: React.FC = ({nickname, src}) => {
6 | return src ? (
7 |
8 |
14 |
15 | ) : (
16 |
17 |
18 | {generateInitials(nickname)}
19 |
20 |
21 | );
22 | };
23 |
24 | export default Avatar;
25 |
--------------------------------------------------------------------------------
/packages/ui/src/Avatar/Avatar.utils.ts:
--------------------------------------------------------------------------------
1 | export const generateInitials = (nickname: string): string => {
2 | const nicknameSplit = nickname.trim().split(" ");
3 |
4 | switch (nicknameSplit.length) {
5 | case 1: {
6 | return nicknameSplit[0].slice(0, 2).toUpperCase();
7 | }
8 | case 2: {
9 | return `${nicknameSplit[0][0]}${nicknameSplit[1][0]}`.toUpperCase();
10 | }
11 | default: {
12 | return `${nicknameSplit[0][0]}${
13 | nicknameSplit[nicknameSplit.length - 1][0]
14 | }`.toUpperCase();
15 | }
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/packages/ui/src/Avatar/index.ts:
--------------------------------------------------------------------------------
1 | export {default as Avatar} from "./Avatar";
2 |
--------------------------------------------------------------------------------
/packages/ui/src/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/packages/ui/src/index.ts:
--------------------------------------------------------------------------------
1 | /* Components */
2 | export * from "./Avatar";
3 |
--------------------------------------------------------------------------------
/packages/ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require("@cool-stack/configs/tailwind/tailwind.config.js")],
3 | content: ["./src/**/*.{ts,tsx,mdx}"]
4 | };
5 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cool-stack/configs/tsconfig/react-library.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/page/.env:
--------------------------------------------------------------------------------
1 | NODE_ENV="development"
2 | REMIX_ENV="development"
3 | API_URL="http://localhost:8055"
4 | API_TOKEN="token"
5 |
--------------------------------------------------------------------------------
/page/.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV="development"
2 | REMIX_ENV="development"
3 | API_URL="http://localhost:8055"
4 | API_TOKEN="token"
5 |
--------------------------------------------------------------------------------
/page/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ["prettier", "@typescript-eslint"],
3 | extends: [
4 | "eslint:recommended",
5 | "@remix-run/eslint-config",
6 | "@remix-run/eslint-config/node",
7 | "plugin:prettier/recommended",
8 | "plugin:import/recommended",
9 | "plugin:unicorn/recommended",
10 | "plugin:jest-dom/recommended",
11 | "plugin:react/recommended",
12 | "plugin:react-hooks/recommended"
13 | ],
14 | env: {
15 | node: true
16 | },
17 | ignorePatterns: ["**/*.js"],
18 | parser: "@typescript-eslint/parser",
19 | parserOptions: {
20 | project: "tsconfig.json",
21 | tsconfigRootDir: __dirname,
22 | sourceType: "module"
23 | },
24 | settings: {
25 | react: {
26 | version: "detect"
27 | },
28 | "import/resolver": {
29 | node: {
30 | extensions: [".ts", ".tsx"],
31 | moduleDirectory: ["app", "node_modules"]
32 | }
33 | }
34 | },
35 | rules: {
36 | "no-unused-vars": "off",
37 | "@typescript-eslint/no-unused-vars": "warn",
38 | "react/prop-types": "off",
39 | "jsx-a11y/anchor-has-content": "off",
40 | "jsx-a11y/img-redundant-alt": "off",
41 | "unicorn/text-encoding-identifier-case": "off",
42 | "unicorn/filename-case": "off",
43 | "unicorn/no-null": "off",
44 | "unicorn/no-nested-ternary": "off",
45 | "unicorn/no-array-callback-reference": "off",
46 | "unicorn/prevent-abbreviations": [
47 | "warn",
48 | {
49 | replacements: {
50 | args: false,
51 | props: false,
52 | ref: false
53 | }
54 | }
55 | ]
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/page/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /server/index.js
5 | /server/index.js.map
6 | /public/build
7 | preferences.arc
8 | sam.json
9 | sam.yaml
10 | .env
11 |
12 | app/styles/*
13 |
--------------------------------------------------------------------------------
/page/.prettierrc:
--------------------------------------------------------------------------------
1 | "@tdsoft/prettier-config"
2 |
--------------------------------------------------------------------------------
/page/app.arc:
--------------------------------------------------------------------------------
1 | @app
2 | cool-stack-page
3 |
4 | @http
5 | /*
6 | method any
7 | src server
8 |
9 | @static
10 |
11 | @aws
12 | profile default
13 | region us-east-1
14 |
--------------------------------------------------------------------------------
/page/app/api/index.ts:
--------------------------------------------------------------------------------
1 | import {Directus} from "@directus/sdk";
2 | import type {Collections} from "@cool-stack/api";
3 |
4 | export const $api = new Directus(process.env.API_URL!, {
5 | auth: {staticToken: process.env.API_TOKEN!}
6 | });
7 |
--------------------------------------------------------------------------------
/page/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {RemixBrowser} from "@remix-run/react";
3 | import {hydrateRoot} from "react-dom/client";
4 |
5 | hydrateRoot(document, );
6 |
--------------------------------------------------------------------------------
/page/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type {EntryContext} from "@remix-run/node";
3 | import {RemixServer} from "@remix-run/react";
4 | import {renderToString} from "react-dom/server";
5 |
6 | export default function handleRequest(
7 | request: Request,
8 | responseStatusCode: number,
9 | responseHeaders: Headers,
10 | remixContext: EntryContext
11 | ) {
12 | const markup = renderToString( );
13 |
14 | responseHeaders.set("Content-Type", "text/html");
15 |
16 | return new Response("" + markup, {
17 | status: responseStatusCode,
18 | headers: responseHeaders
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/page/app/root.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Links,
4 | LiveReload,
5 | Meta,
6 | Outlet,
7 | Scripts,
8 | ScrollRestoration,
9 | useCatch
10 | } from "@remix-run/react";
11 | import type {MetaFunction, LinksFunction, ErrorBoundaryComponent} from "@remix-run/node";
12 | import type {CatchBoundaryComponent} from "@remix-run/react/dist/routeModules";
13 |
14 | import tailwindStylesheetUrl from "./styles/style.css";
15 |
16 | export const meta: MetaFunction = () => ({
17 | charset: "utf-8",
18 | title: "🧊 Cool Stack",
19 | viewport: "width=device-width,initial-scale=1"
20 | });
21 |
22 | export const links: LinksFunction = () => {
23 | return [
24 | {rel: "stylesheet", href: tailwindStylesheetUrl},
25 | {rel: "shortcut icon", href: "/_static/favicon.ico"}
26 | ];
27 | };
28 |
29 | const App = () => {
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
37 | {/* Hero section */}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
49 |
50 |
51 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | );
91 | };
92 |
93 | export const CatchBoundary: CatchBoundaryComponent = () => {
94 | const caught = useCatch();
95 |
96 | return (
97 |
98 |
99 | Oops!
100 |
101 |
102 |
103 |
104 |
105 | CatchBoundary: {caught.data}
106 |
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export const ErrorBoundary: ErrorBoundaryComponent = ({error}) => {
114 | return (
115 |
116 |
117 | Oh no! Something went wrong...
118 |
119 |
120 |
121 |
122 |
123 |
124 | ErrorBoundary: {error.toString()}
125 |
126 |
127 |
128 |
129 | );
130 | };
131 |
132 | export default App;
133 |
--------------------------------------------------------------------------------
/page/app/routes/blog/$slug.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import invariant from "tiny-invariant";
3 | import {useLoaderData} from "@remix-run/react";
4 | import {json} from "@remix-run/node";
5 | import type {LoaderFunction} from "@remix-run/node";
6 | import {Collection, isArticle, isUser, Status} from "@cool-stack/api";
7 | import type {Article, User} from "@cool-stack/api";
8 | import {Avatar} from "@cool-stack/ui";
9 | import {$api} from "~/api";
10 |
11 | interface LoaderData {
12 | article: Article & {author: User};
13 | }
14 |
15 | export const loader: LoaderFunction = async ({params}) => {
16 | const articleResponse = await $api.items(Collection.Articles).readByQuery({
17 | fields: ["*", "author.id" as "author", "author.email" as "author"],
18 | filter: {
19 | status: {
20 | _eq: Status.Published
21 | },
22 | slug: {
23 | _eq: params.slug
24 | }
25 | }
26 | });
27 |
28 | const article = articleResponse.data?.[0];
29 |
30 | if (!article) {
31 | throw new Response("Article Not Found", {
32 | status: 404
33 | });
34 | }
35 |
36 | invariant(isArticle(article) && isUser(article.author), "Error while loading article data");
37 |
38 | return json({
39 | article: article as LoaderData["article"]
40 | });
41 | };
42 |
43 | export default function ArticlePost() {
44 | const {article} = useLoaderData();
45 |
46 | return (
47 |
48 |
49 |
50 |
51 | {new Date(article.date_updated || article.date_created).toLocaleDateString(
52 | "en-US"
53 | )}
54 |
55 |
56 | {article.title}
57 |
58 |
59 |
60 |
61 |
62 |
63 | {article.author.email}
64 |
65 |
66 |
67 |
68 |
69 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/page/app/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {$api} from "~/api";
3 | import {Link, useLoaderData} from "@remix-run/react";
4 | import {json} from "@remix-run/node";
5 | import {Collection, isArticle} from "@cool-stack/api";
6 | import type {Article} from "@cool-stack/api";
7 | import type {LoaderFunction} from "@remix-run/node";
8 |
9 | interface LoaderData {
10 | articles: Array;
11 | }
12 |
13 | export const loader: LoaderFunction = async () => {
14 | const articles = await $api.items(Collection.Articles).readByQuery({
15 | sort: ["-date_created"],
16 | limit: 10
17 | });
18 |
19 | return json({
20 | articles: articles.data?.filter(isArticle) ?? []
21 | });
22 | };
23 |
24 | export default function Index() {
25 | const {articles} = useLoaderData();
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Articles from Directus
36 |
37 |
38 | Lorem Ipsum is simply dummy text of the printing and typesetting
39 | industry. Lorem Ipsum has been the industry's standard dummy text
40 | ever since the 1500s, when an unknown printer took a galley of type
41 | and scrambled it to make a type specimen book. It has survived not
42 | only five centuries, but also the leap into electronic typesetting,
43 | remaining essentially unchanged.
44 |
45 |
46 |
47 |
48 |
49 | {articles.map((article) => (
50 |
51 |
52 | {new Date(
53 | article.date_updated || article.date_created
54 | ).toLocaleDateString("en-US")}
55 |
56 |
57 |
58 | {article.title}
59 |
60 |
61 | Read full
62 |
63 |
64 |
65 | ))}
66 |
67 |
68 |
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/page/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig} from "cypress";
2 |
3 | export default defineConfig({
4 | e2e: {
5 | setupNodeEvents: (on, config) => {
6 | const configOverrides: Partial = {
7 | baseUrl: "http://localhost:3333",
8 | video: false,
9 | screenshotOnRunFailure: false
10 | };
11 |
12 | // To use this:
13 | // cy.task('log', whateverYouWantInTheTerminal)
14 | on("task", {
15 | log: (message) => {
16 | console.log(message);
17 |
18 | return null;
19 | }
20 | });
21 |
22 | return {...config, ...configOverrides};
23 | }
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/page/cypress/e2e/smoke.cy.ts:
--------------------------------------------------------------------------------
1 | describe("💨 Smoke tests", () => {
2 | it("should have a local instance of Directus running", () => {
3 | const defaultDirectusUrl = "http://localhost:8055";
4 | cy.request(`${defaultDirectusUrl}/server/info`).its("isOkStatusCode");
5 | });
6 |
7 | it("should visit the page and navigate to the first article", () => {
8 | cy.visit("");
9 | cy.contains("a > p", "Read full").click();
10 | cy.url().should("match", /.*blog/);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/page/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/page/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************
3 | // This example commands.ts shows you how to
4 | // create various custom commands and overwrite
5 | // existing commands.
6 | //
7 | // For more comprehensive examples of custom
8 | // commands please read more here:
9 | // https://on.cypress.io/custom-commands
10 | // ***********************************************
11 | //
12 | //
13 | // -- This is a parent command --
14 | // Cypress.Commands.add('login', (email, password) => { ... })
15 | //
16 | //
17 | // -- This is a child command --
18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
19 | //
20 | //
21 | // -- This is a dual command --
22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
23 | //
24 | //
25 | // -- This will overwrite an existing command --
26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
27 | //
28 | // declare global {
29 | // namespace Cypress {
30 | // interface Chainable {
31 | // login(email: string, password: string): Chainable
32 | // drag(subject: string, options?: Partial): Chainable
33 | // dismiss(subject: string, options?: Partial): Chainable
34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
35 | // }
36 | // }
37 | // }
38 |
--------------------------------------------------------------------------------
/page/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable unicorn/prevent-abbreviations */
2 | // ***********************************************************
3 | // This example support/e2e.ts is processed and
4 | // loaded automatically before your test files.
5 | //
6 | // This is a great place to put global configuration and
7 | // behavior that modifies Cypress.
8 | //
9 | // You can change the location of this file or turn off
10 | // automatically serving support files with the
11 | // 'supportFile' configuration option.
12 | //
13 | // You can read more here:
14 | // https://on.cypress.io/configuration
15 | // ***********************************************************
16 |
17 | import "@testing-library/cypress/add-commands";
18 | import "./commands";
19 |
20 | Cypress.on("uncaught:exception", (error) => {
21 | // Cypress and React Hydrating the document don't get along
22 | // for some unknown reason. Hopefully we figure out why eventually
23 | // so we can remove this.
24 | if (
25 | /hydrat/i.test(error.message) ||
26 | /Minified React error #418/.test(error.message) ||
27 | /Minified React error #423/.test(error.message)
28 | ) {
29 | return false;
30 | }
31 | });
32 |
--------------------------------------------------------------------------------
/page/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cool-stack/configs/tsconfig/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "noEmit": true,
6 | "types": ["node", "cypress", "@testing-library/cypress"],
7 | "esModuleInterop": true,
8 | "jsx": "react-jsx",
9 | "moduleResolution": "node",
10 | "target": "ES2019",
11 | "strict": true,
12 | "skipLibCheck": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": false,
15 | },
16 | "include": [
17 | "e2e/**/*",
18 | "support/**/*",
19 | "../node_modules/cypress",
20 | "../node_modules/@testing-library/cypress"
21 | ],
22 | "exclude": [
23 | "../node_modules/@types/jest",
24 | "../node_modules/@testing-library/jest-dom"
25 | ]
26 | }
--------------------------------------------------------------------------------
/page/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ...require("@cool-stack/configs/jest/jest-common"),
3 | rootDir: "./"
4 | };
5 |
--------------------------------------------------------------------------------
/page/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@cool-stack/page",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "sideEffects": false,
7 | "scripts": {
8 | "dev": "concurrently \"yarn run dev:remix\" \"yarn run dev:arc\" \"yarn run dev:css\"",
9 | "dev:remix": "remix build && open-cli http://localhost:3333 && remix watch",
10 | "dev:arc": "arc sandbox",
11 | "dev:css": "yarn generate:css --watch",
12 | "generate:css": "tailwindcss -i ../packages/ui/src/global.css -o ./app/styles/style.css --minify",
13 | "build": "yarn generate:css && remix build",
14 | "clean": "rimraf server/index.js && rimraf public/build",
15 | "lint": "cross-env TIMING=1 eslint 'app/**/*.{js,jsx,ts,tsx}'",
16 | "test": "jest",
17 | "test:e2e:dev": "cypress open",
18 | "test:e2e:run": "start-test \"yarn --cwd ../api dev\" 8055 dev 3333 \"npx cypress run\""
19 | },
20 | "dependencies": {
21 | "@cool-stack/api": "*",
22 | "@cool-stack/configs": "*",
23 | "@cool-stack/ui": "*",
24 | "@directus/sdk": "10.3.5",
25 | "@remix-run/architect": "1.19.3",
26 | "@remix-run/node": "1.19.3",
27 | "@remix-run/react": "1.19.3",
28 | "cross-env": "7.0.3",
29 | "react": "18.2.0",
30 | "react-dom": "18.2.0",
31 | "tiny-invariant": "1.3.3"
32 | },
33 | "devDependencies": {
34 | "@architect/architect": "10.16.3",
35 | "@remix-run/dev": "1.19.3",
36 | "@remix-run/eslint-config": "1.19.3",
37 | "@testing-library/cypress": "9.0.0",
38 | "@testing-library/jest-dom": "5.17.0",
39 | "@testing-library/react": "13.4.0",
40 | "@types/jest": "29.5.12",
41 | "@types/react": "18.0.26",
42 | "@types/react-dom": "18.0.9",
43 | "@typescript-eslint/eslint-plugin": "5.62.0",
44 | "@typescript-eslint/parser": "5.62.0",
45 | "aws-sdk": "2.1691.0",
46 | "concurrently": "7.6.0",
47 | "cypress": "12.17.4",
48 | "eslint": "8.57.0",
49 | "eslint-config-prettier": "8.10.0",
50 | "eslint-plugin-import": "2.30.0",
51 | "eslint-plugin-jest-dom": "4.0.3",
52 | "eslint-plugin-prettier": "4.2.1",
53 | "eslint-plugin-react": "7.35.2",
54 | "eslint-plugin-react-hooks": "4.6.2",
55 | "eslint-plugin-unicorn": "45.0.2",
56 | "jest": "29.7.0",
57 | "jest-environment-jsdom": "29.7.0",
58 | "open-cli": "7.2.0",
59 | "prettier": "2.8.8",
60 | "prettier-plugin-tailwindcss": "0.6.6",
61 | "rimraf": "3.0.2",
62 | "start-server-and-test": "1.15.4",
63 | "tailwindcss": "3.4.10",
64 | "ts-jest": "29.2.5",
65 | "typescript": "4.9.5"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/page/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tdsoftpl/cool-stack/bed7a407022d217ff818857c1263a4ba5f9d2b5c/page/public/favicon.ico
--------------------------------------------------------------------------------
/page/public/images/cool-stack-cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tdsoftpl/cool-stack/bed7a407022d217ff818857c1263a4ba5f9d2b5c/page/public/images/cool-stack-cover.png
--------------------------------------------------------------------------------
/page/remix.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | serverBuildTarget: "arc",
3 | server: "./server.js",
4 | ignoredRouteFiles: ["**/.*"],
5 | cacheDirectory: "./node_modules/.cache/remix",
6 | serverDependenciesToBundle: [
7 | "@cool-stack/api", //used only for model types and it's type guards
8 | "@cool-stack/ui"
9 | ],
10 | appDirectory: "app",
11 | assetsBuildDirectory: "public/build",
12 | serverBuildPath: "server/index.js",
13 | publicPath: "/_static/build/",
14 | devServerPort: 3334
15 | };
16 |
--------------------------------------------------------------------------------
/page/remix.env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/page/server.js:
--------------------------------------------------------------------------------
1 | import {createRequestHandler} from "@remix-run/architect";
2 | import * as build from "@remix-run/dev/server-build";
3 |
4 | export const handler = createRequestHandler({
5 | build,
6 | mode: process.env.NODE_ENV
7 | });
8 |
--------------------------------------------------------------------------------
/page/server/config.arc:
--------------------------------------------------------------------------------
1 | @aws
2 | runtime nodejs14.x
3 | # memory 1152
4 | # timeout 30
5 | # concurrency 1
6 |
--------------------------------------------------------------------------------
/page/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require("@cool-stack/configs/tailwind/tailwind.config")],
3 | content: ["./app/**/*.{ts,tsx,jsx,js}", "../packages/ui/src/**/*.{ts,tsx,jsx,js}"]
4 | };
5 |
--------------------------------------------------------------------------------
/page/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@cool-stack/configs/tsconfig/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "allowJs": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
8 | "strict": true,
9 | "target": "ES2019",
10 | "esModuleInterop": true,
11 | "isolatedModules": true,
12 | "jsx": "react-jsx",
13 | "moduleResolution": "node",
14 | "noEmit": true,
15 | "resolveJsonModule": true,
16 | "paths": {
17 | "~/*": ["./app/*"]
18 | }
19 | },
20 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
21 | "exclude": ["node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "semanticCommits": true,
3 | "requiredStatusChecks": null,
4 | "additionalBranchPrefix": "{{parentDir}}-",
5 | "packageRules": [{
6 | "updateTypes": ["minor", "patch", "pin", "digest"],
7 | "automerge": true
8 | }],
9 | "extends": [
10 | "config:base"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turborepo.org/schema.json",
3 | "pipeline": {
4 | "dev": {
5 | "cache": false
6 | },
7 | "clean": {
8 | "cache": false
9 | },
10 | "build": {
11 | "dependsOn": [
12 | "^build"
13 | ],
14 | "outputs": [
15 | "build/**",
16 | "public/build/**",
17 | "src/style.css"
18 | ]
19 | },
20 | "test": {
21 | "dependsOn": [
22 | "^build"
23 | ],
24 | "outputs": [
25 | "coverage/**"
26 | ]
27 | },
28 | "lint": {
29 | "outputs": []
30 | },
31 | "generate:css": {
32 | "outputs": []
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------