Skip to content

Commit

Permalink
feat: add guidelines to landing page
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelovicentegc committed Jun 24, 2024
1 parent b5e00e3 commit fb66a5d
Show file tree
Hide file tree
Showing 18 changed files with 581 additions and 539 deletions.
21 changes: 13 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
SECRET_KEY=
# Copy this file to .env and fill in the values

CDN_NAME=
CDN_API_KEY=
CDN_API_SECRET=
# Generate a secrety key for Django and set it here.
# You can use the following command to generate a secret key:
# python3 -c "import secrets; print(secrets.token_urlsafe())"
SECRET_KEY=

# Set the following variables to the values of your database
DB_HOST=
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_PORT=

# Set the following variables to the values of your Cloudinary account
CDN_NAME=
CDN_API_KEY=
CDN_API_SECRET=

# Set the following variables to the values of your SMTP server
SMTP_HOST_USER=
SMTP_HOST_PASSWORD=

# Wether to run the application in test mode or not
TEST=

TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_WPP_NUMBER=
118 changes: 49 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,31 @@

<p align="center">
<img alt="django-react-typescript logo" src="assets/Logo.png" />
<p align="center">Fully-featured, Django v5 + React v18 boilerplate with great DX.</p>
<p align="center">Fully-featured, Django 5 + React 18 boilerplate with great DX.</p>
</p>

---

This is a fully-featured Django + React boilerplate built for great development experience and easy deployment guidelines.
This is an opinionated fully-featured Django + React boilerplate built for great development experience and easy deployment guidelines.

It is ideal if you want to bootstrap a blog or a portfolio website quickly, or even a more complex application that requires a backend and a frontend, while leveraging the best from React and Django.

- [Getting started](#getting-started)
- [Setting up a database](#setting-up-a-database)
- [Setting up a CDN](#setting-up-a-cdn)
- [Running the project](#running-the-project)
- [Application architecture \& features](#application-architecture--features)
- [Frontend](#frontend)
- [Backend](#backend)
- [Integrations](#integrations)
- [Infrastructure \& deployment](#infrastructure--deployment)
- [Virtualized Deploy Workflow](#virtualized-deploy-workflow)
- [Bare-metal Deploy Workflow](#bare-metal-deploy-workflow)
- [Configuration](#configuration)
- [Global](#global)
- [Exclusively used in production](#exclusively-used-in-production)
- [Frontend](#frontend-1)
- [Exclusively used in production](#exclusively-used-in-production-1)

## Getting started

Expand All @@ -30,6 +49,10 @@ For convenience, if you want to use Docker + Docker Compose to spin up a Postgre
pnpm run dev:db:up
```

### Setting up a CDN

This project uses Cloudinary as a CDN, so you will need to have an account on Cloudinary and set up the `.env` file with the correct credentials. Use the [`.env.example`](./.env.example) file as a reference.

### Running the project

Once you've set up the database, you can start the project by running one of:
Expand All @@ -41,7 +64,7 @@ pnpm dev:full # Starts the project while assuming you've setup a database using

By default, the frontend app will run on `localhost:4000` and the backend app will run on `localhost:8000`. If you're running the containerized Postgres, it will run on `localhost:5432` and pgAdmin will run on `localhost:5050`.

## Application architecture
## Application architecture & features

This application's architect is quite simple and leverages the best of both Django and React. On a nutshell, React and Django integrate through Django's Views and Django Rest Framework's API endpoints.

Expand All @@ -60,67 +83,42 @@ flowchart TD
n0 -- Consumes API Key\nto authenticate\nwith backend --> ng
```

### Global

- Commit lint rules
Below you will find the stack used for each part of the application and the features that are already implemented.

### Frontend

- [React](https://reactjs.org/)
- [Typescript](https://www.typescriptlang.org/)
- [React Router](https://reactrouter.com/)
- [Webpack](https://webpack.js.org/)

| Other features | Status |
| --------------------------- | ----------- |
| SSR ready | In progress |
| Service workers | ✔️ |
| Gzip static file gen | ✔️ |
| Cache control | ✔️ |
| Code split and lazy loading | ✔️ |
| Google Analytics ready | ✔️ |
| PWA ready | ✔️ |
Stack:
- React 18
- React Router 6
- Typescript 5
- Webpack 5
- Tailwind CSS 3

### Backend

- [Django](https://www.djangoproject.com/)
- [Django REST Framework](https://www.django-rest-framework.org/)
- Django CORS Headers
Stack:
- Django 5
- Django Rest Framework
- Postgres

| Other features | Status |
| -------------------- | ------ |
| Token authentication | ✔️ |
| SMTP ready | ✔️ |
### Integrations

### Infrastructure
- [Sentry](https://sentry.io/welcome/)
- [Cloudinary](https://cloudinary.com/)

- Docker image featuring
- [Memcached](https://memcached.org/)
- [PostgreSQL](https://www.postgresql.org/)
- [Supervisor](http://supervisord.org/) (optional, should be used if you're deploying on a non-virtualized system)
## Infrastructure & deployment

| Other features | Status |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| NGINX config file | ✔️ |
| CI/CD to any V.M. (AWS EC2s, GCloud apps, Digital Ocean droplets, Hostgator VPSs, etc) accessible via SSH (the `hml` and `prd` branches will trigger the [deploy workflow](#Virtualized-Deploy-Workflow)) | ✔️ |
| CI/CD to deploy straight on host (without virtualization; not recommended) (the branch `prd-host` will trigger this. See more on the [host deploy workflow](#Host-Deploy-Workflow) method) | ✔️ |
Although this project provides some guidelines on how to deploy the app, it is not mandatory to follow them. You can deploy the app on any platform you want, as long as it supports Docker and Docker Compose, or even deploy the app on a bare-metal machine.

### Integrations
This codebase has two deploy methods available via GitHub actions:

- [Sentry](https://sentry.io/welcome/)
- [Cloudinary](https://cloudinary.com/)
- [Twilio](https://www.twilio.com/)
- [Google Analytics](https://analytics.google.com/analytics/web/)
### Virtualized Deploy Workflow

The `virtualized-deploy-qa` and `virtualized-deploy-prod` branches will trigger this wokflow. You can use it to deploy the app to any Virtual Machine accessible via SSH (AWS EC2s, GCloud apps, Digital Ocean droplets, Hostgator VPSs, etc), and you would likely want to change the name of these branches to something more meaningful to your project.

## Development directions
### Bare-metal Deploy Workflow

1. Clone this repo: `git clone https://github.com/marcelovicentegc/django-react-typescript.git`
2. Create a virtual environment: `python -m venv venv`
3. Activate it ☝️: `source venv/bin/activate` or `venv\Scripts\activate` if you're on a Windows
4. Install dependencies: `npm i && pip install -r requirements.txt && cd frontend && npm i`
5. Setup the project `.env` file by taking as example the `.env.example` on the root folder (refer to [configuration](#Configuration) for more details)
6. Setup the frontend app's `frontend/.env` file by taking as example the `frontend/.env.example` file (refer to [configuration](#Configuration) for more details)
7. Start the application: `npm start` (make sure Postgres is up and running)
The `bare-metail-deploy-qa` and `bare-metal-deploy-prod` branches will trigger this workflow. You can use it to deploy the app straight on the host machine, without any virtualization. This is not recommended, but ou never know when you will need to deploy an app on a bare-metal machine 🤷‍♀️

## Configuration

Expand All @@ -142,9 +140,6 @@ You should configure these variables on a `.env` file on the root folder for the
| SMTP_HOST_USER | | Your SMTP email (should be a GMail one) |
| SMTP_HOST_PASSWORD | - | Your SMTP email password |
| TEST | 0 | Used to test the app on the pipeline |
| TWILIO_ACCOUNT_SID | - | Your Twilio account SID (**optional**) |
| TWILIO_AUTH_TOKEN | - | Your Twilio account Auth token(**optional**) |
| TWILIO_WPP_NUMBER | - | Your Twilio account's Whatsapp number (**optional**) |

#### Exclusively used in production

Expand All @@ -167,7 +162,7 @@ You should configure these variables on a `.env` file on the root folder for the
| -------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| NODE_ENV | `development` | Let's Webpack know when to build files to correct public path, optimize code and when to prepend localhost for API endpoints or not. Values must be either `development` or `production`. This is hardcoded on the [Dockerfile](./Dockerfile) |
| AUTH_TOKEN | - | An auth key generated on Django's admin that must be associated to a user with specific permissions (i.g.: read specific infos from Django's ORM) |
| GTAG_ID | - | Google Analytics ID |
| |

#### Exclusively used in production

Expand All @@ -176,18 +171,3 @@ You should configure these variables on a `.env` file on the root folder for the
| HML_AUTH_KEY | Same as AUTH_KEY but for a HML environment |
| HML_GTAG_ID | Same as GTAG_ID but for a HML environment |

## Deployment worfklows

### Virtualized Deploy Workflow

Branches `hml` and `prd` will trigger this workflow.

![Deploy workflow](./assets/DeployWorkflow.jpg)

### Host Deploy Workflow

For this kind of deploy to work, you will need a running Postgres database, Nginx, and Supervisor processes.

## Basic architecture

![Architecture](./assets/Architecture.png)
3 changes: 1 addition & 2 deletions backend/admin/publications.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from django.utils.html import format_html
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
from backend.models.publications import Publication
from backend.actions import ExportCsvMixin
from backend.utils import Strings

@admin.register(Publication)
class PublicationAdmin(ModelAdmin, DynamicArrayMixin):
class PublicationAdmin(admin.ModelAdmin, DynamicArrayMixin):
def image_preview(self, obj):
return format_html('<img src="{}" style="height: 150px" />'.format(obj.image.url))

Expand Down
3 changes: 1 addition & 2 deletions backend/admin/subscribers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from backend.models.subscribers import Subscriber
from backend.actions import ExportCsvMixin


@admin.register(Subscriber)
class SubscriberAdmin(ModelAdmin, ExportCsvMixin):
class SubscriberAdmin(admin.ModelAdmin, ExportCsvMixin):
list_filter = ('contact_method', 'created_at')
actions = ["export_as_csv"]
search_fields = ['name']
Expand Down
1 change: 0 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
NODE_ENV=development
AUTH_TOKEN=
GTAG_ID=
15 changes: 7 additions & 8 deletions frontend/lib/api/use-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@ import {
import { getSecrets } from "../config";
import type { IGetPaginatedPublicationsResponse, IPublication } from "./types";

const { NODE_ENV, AUTH_TOKEN } = getSecrets();
const { isProd, authToken } = getSecrets();

const PRODUCTION_MODE = NODE_ENV === "production";
const LOCAL_API_URL = "http://localhost:8000";

export function useApi() {
const getHeaders = new Headers({
Accept: "*/*",
"Accept-Encoding": "gzip, deflate, br",
Authorization: "Token " + AUTH_TOKEN,
Authorization: "Token " + authToken,
});

async function getPublications(
Expand All @@ -32,7 +31,7 @@ export function useApi() {
const endpoint = (() => {
const shouldFilter = args.tag && args.title;

if (PRODUCTION_MODE) {
if (isProd) {
if (shouldFilter) {
return getFilteredPublicationsEndoint(args);
}
Expand Down Expand Up @@ -69,7 +68,7 @@ export function useApi() {
}): Promise<IGetPaginatedPublicationsResponse> {
const endpoint = (() => {
if (!args) {
if (PRODUCTION_MODE) {
if (isProd) {
return getPaginatedPublicationsEndpoint;
}

Expand All @@ -81,7 +80,7 @@ export function useApi() {
}

if (args.page && !args.filter) {
if (PRODUCTION_MODE) {
if (isProd) {
return getPaginatedPublicationsEndpoint + `?page=${args.page}`;
}

Expand All @@ -91,7 +90,7 @@ export function useApi() {
`?page=${args.page}`
);
} else if (args.filter) {
if (PRODUCTION_MODE) {
if (isProd) {
return getPaginatedFilteredPublicationsEndoint({
title: args.filter.title,
tag: args.filter.tags,
Expand Down Expand Up @@ -122,7 +121,7 @@ export function useApi() {

async function getPublication(slug: string): Promise<IPublication> {
return fetch(
PRODUCTION_MODE
isProd
? getPublicationEndpoint(slug)
: LOCAL_API_URL + getPublicationEndpoint(slug),
{
Expand Down
50 changes: 50 additions & 0 deletions frontend/lib/components/blog-post-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";
import { Button, Card } from "flowbite-react";
import { IPublication } from "../api";
import dayjs from "dayjs";
import { ROUTES, useRouter } from "../routes";

interface Props {
data: IPublication;
}

export function BlogPostPreview(props: Props) {
const { data } = props;
const { push } = useRouter();

return (
<Card
className="max-w-sm mt-4"
imgAlt={data.image_description}
imgSrc={data.image}
>
<h5 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{data.title} {dayjs(data.created_at).locale("pt-br").format("LLLL")}
</h5>
<p className="font-normal text-gray-700 dark:text-gray-400">
{getPreview(data)}
</p>
<Button onClick={() => push(ROUTES.BLOG + "/" + data.slug)}>
Read more
<svg
className="-mr-1 ml-2 h-4 w-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Button>
</Card>
);
}

function getPreview(data: IPublication) {
const preview = data.description ? data.description : data.body;

return preview.slice(0, 50);
}
Loading

0 comments on commit fb66a5d

Please sign in to comment.