Merge branch 'main' into configuration-checking

This commit is contained in:
James Read 2024-09-10 20:15:03 +01:00 committed by GitHub
commit 5e9dfa0172
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 1204 additions and 528 deletions

View File

@ -0,0 +1,14 @@
{
"name": "Postiz Dev Container",
"image": "localhost/postiz-devcontainer",
"features": {},
"customizations": {
"vscode": {
"settings": {},
"extensions": []
}
},
"forwardPorts": ["4200:4200", "3000:3000"],
"mounts": ["source=/apps,destination=/apps/dist/,type=bind,consistency=cached"]
}

13
.dockerignore Normal file
View File

@ -0,0 +1,13 @@
# We want the docker builds to be clean, and as fast as possible. Don't send
# any half-built stuff in the build context as a pre-caution (also saves copying
# 180k files in node_modules that isn't used!).
**/node_modules
dist
.nx
.devcontainer
**/.git
**/dist
**/*.md
**/LICENSE
**/npm-debug.log
**/*.vscode

43
.github/workflows/build-containers.yml vendored Normal file
View File

@ -0,0 +1,43 @@
---
name: "Build Containers"
on:
workflow_dispatch:
push:
tags:
- '*'
jobs:
build-containers:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Login to ghcr
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: docker build
run: ./var/docker/docker-build.sh
- name: Get date
run: |
echo "DATE=$(date +'%s')" >> "$GITHUB_ENV"
- name: Print post-build debug info
run: |
docker images
- name: docker tag
run: |
docker tag localhost/postiz ghcr.io/gitroomhq/postiz-app:${{ env.DATE }}
docker push ghcr.io/gitroomhq/postiz-app:${{ env.DATE }}
docker tag localhost/postiz-devcontainer ghcr.io/gitroomhq/postiz-devcontainer:${{ env.DATE }}
docker push ghcr.io/gitroomhq/postiz-devcontainer:${{ env.DATE }}

52
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,52 @@
---
name: Build
on:
push:
branches:
- main
paths:
- package.json
- apps/**
- '!apps/docs/**'
- libraries/**
pull_request:
paths:
- package.json
- apps/**
- '!apps/docs/**'
- libraries/**
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['20.17.0']
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: |
**/package-lock.json
# https://nextjs.org/docs/pages/building-your-application/deploying/ci-build-caching#github-actions
- uses: actions/cache@v4
with:
path: |
~/.npm
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
- run: npm ci
- run: npm run build

47
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,47 @@
---
name: "Code Quality Analysis"
on:
push:
branches:
- main
paths:
- apps/**
- '!apps/docs/**'
- libraries/**
pull_request:
paths:
- apps/**
- '!apps/docs/**'
- libraries/**
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: 'ubuntu-latest'
permissions:
security-events: write
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

60
.github/workflows/eslint.yaml vendored Normal file
View File

@ -0,0 +1,60 @@
---
name: ESLint
on:
push:
branches:
- main
paths:
- package.json
- apps/**
- '!apps/docs/**'
- libraries/**
pull_request:
paths:
- package.json
- apps/**
- '!apps/docs/**'
- libraries/**
jobs:
eslint:
name: Run eslint scanning
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
strategy:
matrix:
service: ["backend", "frontend"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: |
**/package-lock.json
- name: Install ESLint
run: |
npm install eslint
npm install @microsoft/eslint-formatter-sarif@2.1.7
- name: Run ESLint
run: npx eslint apps/${{ matrix.service }}/
--config apps/${{ matrix.service }}/.eslintrc.json
--format @microsoft/eslint-formatter-sarif
--output-file apps/${{ matrix.service }}/eslint-results.sarif
continue-on-error: true
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: apps/${{ matrix.service }}/eslint-results.sarif
wait-for-processing: true

4
.gitignore vendored
View File

@ -42,3 +42,7 @@ Thumbs.db
# Next.js
.next
# Vim files
**/*.swp
**/*.swo

58
Dockerfile.dev Normal file
View File

@ -0,0 +1,58 @@
# This Dockerfile is used for producing 3 container images.
#
# base - which is thrown away, that contains node and the basic infrastructure.
# devcontainer - which is used for development, and contains the source code and the node_modules.
# dist - which is used for production, and contains the built source code and the node_modules.
ARG NODE_VERSION="20.17"
# Base image
FROM docker.io/node:${NODE_VERSION}-alpine3.19 AS base
## Just reduce unccessary noise in the logs.
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
ENV NEXT_TELEMETRY_DISABLED=1
RUN apk add --no-cache \
bash=5.2.21-r0 \
supervisor=4.2.5-r4 \
make \
build-base
WORKDIR /app
EXPOSE 4200
EXPOSE 3000
COPY var/docker/entrypoint.sh /app/entrypoint.sh
COPY var/docker/supervisord.conf /etc/supervisord.conf
COPY var/docker/supervisord /app/supervisord_available_configs/
COPY .env.example /config/.env
VOLUME /config
LABEL org.opencontainers.image.source=https://github.com/gitroomhq/postiz-app
ENTRYPOINT ["/app/entrypoint.sh"]
# Builder image
FROM base AS devcontainer
COPY nx.json tsconfig.base.json package.json package-lock.json /app/
COPY apps /app/apps/
COPY libraries /app/libraries/
RUN npm ci --no-fund && npx nx run-many --target=build --projects=frontend,backend,workers,cron
LABEL org.opencontainers.image.title="Postiz App (DevContainer)"
# Output image
FROM base AS dist
COPY --from=devcontainer /app/node_modules/ /app/node_modules/
COPY --from=devcontainer /app/dist/ /app/dist/
COPY package.json nx.json /app/
## Labels at the bottom, because CI will eventually add dates, commit hashes, etc.
LABEL org.opencontainers.image.title="Postiz App (Production)"

View File

@ -48,19 +48,18 @@ export class PostsController {
@GetOrgFromRequest() org: Organization,
@Query() query: GetPostsDto
) {
const [posts, comments] = await Promise.all([
const [posts] = await Promise.all([
this._postsService.getPosts(org.id, query),
this._commentsService.getAllCommentsByWeekYear(
org.id,
query.year,
query.week,
query.isIsoWeek === 'true'
),
// this._commentsService.getAllCommentsByWeekYear(
// org.id,
// query.year,
// query.week
// ),
]);
return {
posts,
comments,
// comments,
};
}

View File

@ -169,4 +169,4 @@ You can look at the other integration to understand what data to put inside.
And add the new provider to the list.
```typescript show.all.providers.tsx
{identifier: 'providerName', component: DefaultImportFromHighOrderProvider},
```
```

View File

@ -3,14 +3,16 @@ title: Email Notifications
description: How to send notifications to users
---
At the moment we are using Resend to send email notifications to users, and might be changed the Novu later.
Postiz uses Resend to send email notifications to users. Emails are currently
required as part of the new-user creation process, which sends an activation
email.
Register to [Resend](https://resend.com) connect your domain.
Copy your API Key.
Head over to .env file and add the following line.
* Register to [Resend](https://resend.com), and connect your domain.
* Copy your API Key from the Resend control panel.
* Open the .env file and edit the following line.
```env
RESEND_API_KEY=""
RESEND_API_KEY="<your-api-key-here>"
```
Feel free to contribute other providers to send email notifications.
Feel free to contribute other providers to send email notifications.

View File

@ -8,13 +8,13 @@ Unlike other NX project, this project has one `.env` file that is shared between
It makes it easier to develop and deploy the project.<br /><br />
When deploying to websites like [Railway](https://railway.app) or [Heroku](https://heroku.com), you can use a shared environment variables for all the apps.<br /><br />
**At the moment it has 6 project:**
**At the moment it has 6 projects:**
- **Backend** - NestJS based system
- **Workers** - NestJS based workers connected to a Redis Queue.
- **Cron** - NestJS scheduler to run cron jobs.
- **Frontend** - NextJS based control panel.
- **Docs** - Mintlify based documentation website.
- [Frontend](#frontend) - Provides the Web user interface, talks to the Backend.
- [Backend](#backend) - Does all the real work, provides an API for the frontend, and posts work to the redis queue.
- [Workers](#worker) - Consumes work from the Redis Queue.
- [Cron](#cron) - Run jobs at scheduled times.
- [Docs](#docs) - This documentation site!
<img
src="/images/arch.png"

View File

@ -0,0 +1,144 @@
---
title: Development Environment
---
This is currently the recommended option to install Postiz in a supportable configuration. The docker images are in active and heavy development for now.
## Tested configurations
- MacOS
- Linux (Fedora 40)
Naturally you can use these instructions to setup a development environment on any platform, but there may not be much experience in the community to help you with any issues you may encounter.
## Prerequisites
This guide will ask you to install & configure several services exaplained below.
### Prerequisite Cloud Services
- **[Resend account](https://resend.com)** - for user activation and email notifications.
- **[Cloudflare R2](https://cloudfalre.com)** - for uploads (optional, can use local machine), and storing account data.
- **Social Media API details** - various API keys and secrets (more details later) for services you want to use; reddit, X, Instagram, etc..
### Prerequisite Local Services
- **Node.js** - for running the code! (version 18+)
- **PostgreSQL** - or any other SQL database (instructions beleow suggest Docker)
- **Redis** - for handling worker queues (instructions below suggest Docker)
We have some messages from users who are using Windows, which should work, but they are not tested well yet.
## Installation Instructions
### NodeJS (version 18+)
A complete guide of how to install NodeJS can be found [here](https://nodejs.org/en/download/).
### PostgreSQL (or any other SQL database) & Redis
You can choose **Option A** to **Option B** to install the database.
#### Option A) Postgres and Redis as Single containers
You can install [Docker](https://www.docker.com/products/docker-desktop) and run:
```bash Terminal
docker run -e POSTGRES_USER=root -e POSTGRES_PASSWORD=your_password --name postgres -p 5432:5432 -d postgres
docker run --name redis -p 6379:6379 -d redis
```
#### Option B) Postgres and Redis as docker-compose
Download the [docker-compose.yaml file here](https://raw.githubusercontent.com/gitroomhq/postiz-app/main/docker-compose.dev.yaml),
or grab it from the repository in the next step.
```bash Terminal
docker compose -f "docker-compose.dev.yaml" up
```
## Build Postiz
<Steps>
<Step title="Clone the repository">
```bash Terminal
git clone https://github.com/gitroomhq/gitroom
```
</Step>
<Step title="Set environment variables">
Copy the `.env.example` file to `.env` and fill in the values
```bash .env
# Required Settings
DATABASE_URL="postgresql://postiz-user:postiz-password@localhost:5432/postiz-db-local"
REDIS_URL="redis://localhost:6379"
JWT_SECRET="random string for your JWT secret, make it long"
FRONTEND_URL="http://localhost:4200"
NEXT_PUBLIC_BACKEND_URL="http://localhost:3000"
BACKEND_INTERNAL_URL="http://localhost:3000"
# Social Media API Settings
X_API_KEY="Twitter API key for normal oAuth not oAuth2"
X_API_SECRET="Twitter API secret for normal oAuth not oAuth2"
LINKEDIN_CLIENT_ID="Linkedin Client ID"
LINKEDIN_CLIENT_SECRET="Linkedin Client Secret"
REDDIT_CLIENT_ID="Reddit Client ID"
REDDIT_CLIENT_SECRET="Linkedin Client Secret"
GITHUB_CLIENT_ID="GitHub Client ID"
GITHUB_CLIENT_SECRET="GitHub Client Secret"
RESEND_API_KEY="Resend API KEY"
UPLOAD_DIRECTORY="optional: your upload directory path if you host your files locally"
NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY="optional: your upload directory slug if you host your files locally"
CLOUDFLARE_ACCOUNT_ID="Cloudflare R2 Account ID"
CLOUDFLARE_ACCESS_KEY="Cloudflare R2 Access Key"
CLOUDFLARE_SECRET_ACCESS_KEY="Cloudflare R2 Secret Access Key"
CLOUDFLARE_BUCKETNAME="Cloudflare R2 Bucket Name"
CLOUDFLARE_BUCKET_URL="Cloudflare R2 Backet URL"
# Developer Settings
NX_ADD_PLUGINS=false
IS_GENERAL="true" # required for now
```
</Step>
<Step title="Install the dependencies">
```bash Terminal
npm install
```
</Step>
<Step title="Generate the prisma client and run the migrations">
```bash Terminal
npm run prisma-db-push
```
</Step>
<Step title="Run the project">
```bash Terminal
npm run dev
```
</Step>
</Steps>
If everything is running successfully, open http://localhost:4200 in your browser!
If everything is not running - you had errors in the steps above, please head over to our [support](/support) page.
## Next Steps
<CardGroup cols={2}>
<Card title="How it works" icon="screwdriver-wrench" href="/howitworks">
Learn the architecture of the project
</Card>
<Card title="Email notifications" icon="envelope" href="/emails">
Set up email for notifications
</Card>
<Card title="GitHub" icon="code-branch" href="/github">
Set up github for authentication and sync
</Card>
<Card title="Providers" icon="linkedin" href="/providers/x/x">
Set up providers such as Linkedin, X and Reddit
</Card>
</CardGroup>

View File

@ -0,0 +1,69 @@
---
title: Docker Compose
---
import EarlyDoc from '/snippets/earlydoc.mdx';
import DockerDatabase from '/snippets/docker-database.mdx';
import DockerEnvvarApps from '/snippets/docker-envvar-apps.mdx';
<EarlyDoc />
<DockerDatabase />
# Example `docker-compose.yml` file
```yaml
services:
postiz:
image: ghcr.io/gitroomhq/postiz-app:latest
container_name: postiz
restart: always
volumes:
- ./config:/config/ # Should contain your .env file
ports:
- 4200:4200
- 3000:3000
networks:
- postiz-network
postiz-postgres:
image: postgres:14.5
container_name: postiz-postgres
restart: always
environment:
POSTGRES_PASSWORD: postiz-local-pwd
POSTGRES_USER: postiz-local
POSTGRES_DB: postiz-db-local
volumes:
- postgres-volume:/var/lib/postgresql/data
ports:
- 5432:5432
networks:
- postiz-network
postiz-pg-admin:
image: dpage/pgadmin4
container_name: postiz-pg-admin
restart: always
ports:
- 8081:80
environment:
PGADMIN_DEFAULT_EMAIL: admin@admin.com
PGADMIN_DEFAULT_PASSWORD: admin
networks:
- postiz-network
postiz-redis:
image: redis:7.2
container_name: postiz-redis
restart: always
ports:
- 6379:6379
volumes:
postgres-volume:
external: false
networks:
postiz-network:
external: false
```
<DockerEnvvarApps />

View File

@ -0,0 +1,20 @@
---
title: Docker
---
import EarlyDoc from '/snippets/earlydoc.mdx';
import DockerDatabase from '/snippets/docker-database.mdx';
import DockerEnvvarApps from '/snippets/docker-envvar-apps.mdx';
<EarlyDoc />
<DockerDatabase />
# Create the container on command line
```bash
docker create --name postiz -v ./config:/config -p 4200:4200 -p 3000:3000 ghcr.io/postiz/postiz:latest
```
<DockerEnvvarApps />

View File

@ -0,0 +1,13 @@
---
title: Helm
---
import EarlyDoc from '/snippets/earlydoc.mdx';
import DockerDatabase from '/snippets/docker-database.mdx';
<EarlyDoc />
<DockerDatabase />
Postiz has a helm chart that is in very active development. You can find it here;
https://github.com/gitroomhq/postiz-helmchart

View File

@ -61,6 +61,15 @@
"pages": [
"introduction",
"quickstart",
{
"group": "Install",
"pages": [
"installation/development",
"installation/docker",
"installation/docker-compose",
"installation/kubernetes-helm"
]
},
"howitworks",
"emails",
"github",
@ -73,9 +82,15 @@
"providers/articles"
]
},
"providers/how-to-add-provider"
"support"
]
}
},
{
"group": "Developer Guide",
"pages": [
"developer-guide/how-to-add-provider"
]
}
],
"footerSocials": {
"twitter": "https://twitter.com/nevodavid",

View File

@ -2,117 +2,17 @@
title: 'Quickstart'
---
## Prerequisites
At the moment it is necessary to build the project locally - some dependencies
like redis and postgres can run as docker containers, but there is no docker
container for Postiz just yet.
To run the project you need to have multiple things:
## Self Hosted installation options
- Node.js (version 18+)
- PostgreSQL (or any other SQL database)
- Redis
- Resend account
- Cloudflare R2 for uploads (optional, can use local machine)
- Social Media Client and Secret (more details later)
You can choose between the following installation options;
### NodeJS (version 18+)
* [Development](/installation/development) - The only installation option that is offically supported at the moment.
* [Docker (standalone)](/installation/docker) - Run from the command line with Docker.
* [Docker Compose](/installation/docker-compose) - Run with Docker Compose.
* [Helm](/installation/kubernetes-helm) - Run with Kubernetes + Helm.
A complete guide of how to install NodeJS can be found [here](https://nodejs.org/en/download/).
### PostgreSQL (or any other SQL database)
Make sure you have PostgreSQL installed on your machine.<br />
If you don't, you can install [Docker](https://www.docker.com/products/docker-desktop) and run:
```bash
docker run -e POSTGRES_USER=root -e POSTGRES_PASSWORD=your_password --name postgres -p 5432:5432 -d postgres
```
### Redis
Make sure you have Redis installed on your machine.<br />
If you don't, you can install [Docker](https://www.docker.com/products/docker-desktop) and run:
```bash
docker run --name redis -p 6379:6379 -d redis
```
## Installation
<Steps>
<Step title="Clone the repository">
```bash Terminal
git clone https://github.com/gitroomhq/gitroom
```
</Step>
<Step title="Copy environment variables">
Copy the `.env.example` file to `.env` and fill in the values
```bash .env
DATABASE_URL="postgres database URL"
REDIS_URL="redis database URL"
JWT_SECRET="random string for your JWT secret, make it long"
FRONTEND_URL="By default: http://localhost:4200"
NEXT_PUBLIC_BACKEND_URL="By default: http://localhost:3000"
BACKEND_INTERNAL_URL="If you use docker, you might want something like: http://backend:3000"
X_API_KEY="Twitter API key for normal oAuth not oAuth2"
X_API_SECRET="Twitter API secret for normal oAuth not oAuth2"
LINKEDIN_CLIENT_ID="Linkedin Client ID"
LINKEDIN_CLIENT_SECRET="Linkedin Client Secret"
REDDIT_CLIENT_ID="Reddit Client ID"
REDDIT_CLIENT_SECRET="Linkedin Client Secret"
GITHUB_CLIENT_ID="GitHub Client ID"
GITHUB_CLIENT_SECRET="GitHub Client Secret"
RESEND_API_KEY="Resend API KEY"
UPLOAD_DIRECTORY="optional: your upload directory path if you host your files locally"
NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY="optional: your upload directory slug if you host your files locally"
CLOUDFLARE_ACCOUNT_ID="Cloudflare R2 Account ID"
CLOUDFLARE_ACCESS_KEY="Cloudflare R2 Access Key"
CLOUDFLARE_SECRET_ACCESS_KEY="Cloudflare R2 Secret Access Key"
CLOUDFLARE_BUCKETNAME="Cloudflare R2 Bucket Name"
CLOUDFLARE_BUCKET_URL="Cloudflare R2 Backet URL"
NX_ADD_PLUGINS=false
IS_GENERAL="true" # required for now
```
</Step>
<Step title="Install the dependencies">
```bash Terminal
npm install
```
</Step>
<Step title="Setup postgres & redis via docker compose">
```bash Terminal
docker compose -f "docker-compose.dev.yaml" up
```
</Step>
<Step title="Generate the prisma client and run the migrations">
```bash Terminal
npm run prisma-db-push
```
</Step>
<Step title="Run the project">
```bash Terminal
npm run dev
```
</Step>
</Steps>
You have to follow all the tabs in the "Developers" menu to install Gitroom
<CardGroup cols={2}>
<Card title="How it works" icon="screwdriver-wrench" href="/howitworks">
Learn the architecture of the project
</Card>
<Card title="Email notifications" icon="envelope" href="/emails">
Set up email for notifications
</Card>
<Card title="GitHub" icon="code-branch" href="/github">
Set up github for authentication and sync
</Card>
<Card title="Providers" icon="linkedin" href="/providers/x/x">
Set up providers such as Linkedin, X and Reddit
</Card>
</CardGroup>

View File

@ -0,0 +1,6 @@
<Warning>
The container images do not yet provide automatic database "installation"
(migrations). This must be done manually outside of the docker containers for now.
This is being worked on with a high priority.
</Warning>

View File

@ -0,0 +1,7 @@
## Controlling container services
The environment variable POSTIZ_APPS defaults to "", which means that all
services will be started in a single container. However, you can only start
specific services within the docker container by changing this environement variable.
For most deployments, starting all services is fine. To scale out, you might want
to start individual containers for "frontend", "backend", "worker" and "cron".

View File

@ -0,0 +1,6 @@
<Info>
**NOTE:** This page is marked "earlydoc", or "early documentation", which means it might
be brief, or contain information about parts of the app that are under heavy development.
If you encounter issues with instructions found here, please check out the [support](/support)
page for options.
</Info>

35
apps/docs/support.mdx Normal file
View File

@ -0,0 +1,35 @@
---
title: Support
---
Sometimes, things can go wrong, or you need some help!
Note that the Self Hosted version of Postiz is supported by the community in their
free time, on a best-efforts basis. Please post your question and be patient.
- [Discord](https://discord.com/invite/sf7QjTcX37) - Flexible chat, with screenshots and screen sharing, probably the best option for support.
- [GitHub issue](https://github.com/gitroomhq/postiz-app/issues/new/choose) - backup option if you are unable to use Discord.
## How to effectively ask for support
Try to follow this guide when asking for support, in this order; **Goal, Environment, Changes, Results**.
- **Goal:** Start off by explaining what you were trying to do
- _I want to schedule a post on Reddit at 6pm_
- _I want to run in a Linux container on a Raspberry Pi_
- _I want to use a custom domain name_
- **Environment:** - Share the relevant parts about your environment. Web App issues; Are you using Firefox, Chrome, etc? Installation/Other issues; a Mac, Linux, Windows, how did you install?
- _I'm using Firefox on Windows 10_
- _I'm using a Raspberry Pi 4 with Ubuntu 20.04, and Node version 18_
- _This is a new installation on a Mac_
- **Changed:** - Most likely something has changed, what is it?
- _I updated my browser to the latest version and now ..._
- _I found a change in the latest version and now ..._
- _I think this used to work, but now..._
- **Results:** - What happened? What did you expect to happen?
- _I see a blank screen_
- _I see an error message_
- _I see a 404 page_

View File

@ -17,8 +17,8 @@ export default async function AuthLayout({
<>
<ReturnUrlComponent />
<div className="absolute left-0 top-0 z-[0] h-[100vh] w-[100vw] overflow-hidden bg-loginBg bg-contain bg-no-repeat bg-left-top" />
<div className="relative z-[1] pr-[100px] flex justify-end items-center h-[100vh] w-[100vw] overflow-hidden">
<div className="w-[557px] flex h-[614px] bg-loginBox bg-contain">
<div className="relative z-[1] px-3 lg:pr-[100px] xs:mt-[70px] flex justify-center lg:justify-end items-center h-[100vh] w-[100vw] overflow-hidden">
<div className="w-full max-w-lg h-[614px] flex flex-col bg-loginBox bg-no-repeat bg-contain">
<div className="w-full relative">
<div className="custom:fixed custom:text-left custom:left-[20px] custom:justify-start custom:top-[20px] absolute -top-[100px] text-textColor justify-center items-center w-full flex gap-[10px]">
<Image
@ -61,7 +61,7 @@ export default async function AuthLayout({
</div>
</div>
</div>
<div className="p-[32px] absolute w-[557px] h-[614px] text-textColor">
<div className="p-[32px] w-full h-[614px] text-textColor">
{children}
</div>
<div className="flex flex-1 flex-col">

View File

@ -34,7 +34,7 @@
--color-custom20: #121b2c;
--color-custom21: #506490;
--color-custom22: #b91c1c;
--color-custom23: #06080d;
--color-custom23: #000000;
--color-custom24: #eaff00;
--color-custom25: #2e3336;
--color-custom26: #1d9bf0;

View File

@ -1,5 +1,4 @@
import interClass from '@gitroom/react/helpers/inter.font';
export const dynamic = 'force-dynamic';
import './global.scss';
import 'react-tooltip/dist/react-tooltip.css';

View File

@ -12,7 +12,6 @@ import React, {
import dayjs from 'dayjs';
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
import clsx from 'clsx';
import { commands } from '@uiw/react-md-editor';
import { usePreventWindowUnload } from '@gitroom/react/helpers/use.prevent.window.unload';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useModals } from '@mantine/modals';
@ -27,16 +26,14 @@ import {
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useMoveToIntegration } from '@gitroom/frontend/components/launches/helpers/use.move.to.integration';
import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data';
import { newImage } from '@gitroom/frontend/components/launches/helpers/new.image.component';
import { MultiMediaComponent } from '@gitroom/frontend/components/media/media.component';
import { useExpend } from '@gitroom/frontend/components/launches/helpers/use.expend';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { PickPlatforms } from '@gitroom/frontend/components/launches/helpers/pick.platform.component';
import { ProvidersOptions } from '@gitroom/frontend/components/launches/providers.options';
import { v4 as uuidv4 } from 'uuid';
import useSWR, { useSWRConfig } from 'swr';
import useSWR from 'swr';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { postSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector';
import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow';
import { DatePicker } from '@gitroom/frontend/components/launches/helpers/date.picker';
import { arrayMoveImmutable } from 'array-move';
@ -56,10 +53,10 @@ export const AddEditModal: FC<{
date: dayjs.Dayjs;
integrations: Integrations[];
reopenModal: () => void;
mutate: () => void;
}> = (props) => {
const { date, integrations, reopenModal } = props;
const { date, integrations, reopenModal, mutate } = props;
const [dateState, setDateState] = useState(date);
const { mutate } = useSWRConfig();
// hook to open a new modal
const modal = useModals();
@ -246,7 +243,7 @@ export const AddEditModal: FC<{
await fetch(`/posts/${existingData.group}`, {
method: 'DELETE',
});
mutate('/posts');
mutate();
modal.closeAll();
return;
}
@ -324,7 +321,7 @@ export const AddEditModal: FC<{
existingData.group = uuidv4();
mutate('/posts');
mutate();
toaster.show(
!existingData.integration
? 'Added successfully'

View File

@ -17,16 +17,33 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { Post, Integration } from '@prisma/client';
import { useRouter, useSearchParams } from 'next/navigation';
import { isGeneral } from '@gitroom/react/helpers/is.general';
import isoWeek from 'dayjs/plugin/isoWeek';
import weekOfYear from 'dayjs/plugin/weekOfYear';
dayjs.extend(isoWeek);
dayjs.extend(weekOfYear);
const CalendarContext = createContext({
currentWeek: dayjs().week(),
currentYear: dayjs().year(),
currentMonth: dayjs().month(),
comments: [] as Array<{ date: string; total: number }>,
integrations: [] as Integrations[],
trendings: [] as string[],
posts: [] as Array<Post & { integration: Integration }>,
setFilters: (filters: { currentWeek: number; currentYear: number }) => {},
changeDate: (id: string, date: dayjs.Dayjs) => {},
reloadCalendarView: () => {/** empty **/},
display: 'week',
setFilters: (filters: {
currentWeek: number;
currentYear: number;
currentMonth: number;
display: 'week' | 'month';
}) => {
/** empty **/
},
changeDate: (id: string, date: dayjs.Dayjs) => {
/** empty **/
},
});
export interface Integrations {
@ -40,28 +57,21 @@ export interface Integrations {
}
function getWeekNumber(date: Date) {
// Copy date so don't modify original
const targetDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
// Set to nearest Thursday: current date + 4 - current day number
// Make Sunday's day number 7
targetDate.setUTCDate(targetDate.getUTCDate() + 4 - (targetDate.getUTCDay() || 7));
// Get first day of year
const yearStart = new Date(Date.UTC(targetDate.getUTCFullYear(), 0, 1));
// Calculate full weeks to nearest Thursday
return Math.ceil((((targetDate.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
}
function isISOWeek(date: Date, weekNumber: number): boolean {
// Copy date so don't modify original
const targetDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
// Set to nearest Thursday: current date + 4 - current day number
// Make Sunday's day number 7
targetDate.setUTCDate(targetDate.getUTCDate() + 4 - (targetDate.getUTCDay() || 7));
// Get first day of year
const yearStart = new Date(Date.UTC(targetDate.getUTCFullYear(), 0, 1));
// Calculate full weeks to nearest Thursday
const isoWeekNo = Math.ceil((((targetDate.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
return isoWeekNo === weekNumber;
// Copy date so don't modify original
const targetDate = new Date(
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
);
// Set to nearest Thursday: current date + 4 - current day number
// Make Sunday's day number 7
targetDate.setUTCDate(
targetDate.getUTCDate() + 4 - (targetDate.getUTCDay() || 7)
);
// Get first day of year
const yearStart = new Date(Date.UTC(targetDate.getUTCFullYear(), 0, 1));
// Calculate full weeks to nearest Thursday
return Math.ceil(
((targetDate.getTime() - yearStart.getTime()) / 86400000 + 1) / 7
);
}
export const CalendarWeekProvider: FC<{
@ -70,64 +80,72 @@ export const CalendarWeekProvider: FC<{
}> = ({ children, integrations }) => {
const fetch = useFetch();
const [internalData, setInternalData] = useState([] as any[]);
const [trendings, setTrendings] = useState<string[]>([]);
const { mutate } = useSWRConfig();
const [trendings] = useState<string[]>([]);
const searchParams = useSearchParams();
const router = useRouter();
useEffect(() => {
(async () => {
if (isGeneral()) {
return [];
}
setTrendings(await (await fetch('/posts/predict-trending')).json());
})();
}, []);
const display = searchParams.get('month') ? 'month' : 'week';
const [filters, setFilters] = useState({
currentWeek: +(searchParams.get('week') || getWeekNumber(new Date())),
currentWeek:
display === 'week'
? +(searchParams.get('week') || getWeekNumber(new Date()))
: 0,
currentMonth:
display === 'week' ? 0 : +(searchParams.get('month') || dayjs().month()),
currentYear: +(searchParams.get('year') || dayjs().year()),
display,
});
const isIsoWeek = useMemo(() => {
return isISOWeek(new Date(), filters.currentWeek);
}, [filters]);
const setFiltersWrapper = useCallback(
(filters: { currentWeek: number; currentYear: number }) => {
setFilters(filters);
router.replace(
`/launches?week=${filters.currentWeek}&year=${filters.currentYear}`
);
setTimeout(() => {
mutate('/posts');
});
},
[filters]
);
const params = useMemo(() => {
return new URLSearchParams({
week: filters.currentWeek.toString(),
year: filters.currentYear.toString(),
isIsoWeek: isIsoWeek.toString(),
}).toString();
return new URLSearchParams(
filters.currentWeek
? {
week: filters.currentWeek.toString(),
year: filters.currentYear.toString(),
}
: {
year: filters.currentYear.toString(),
month: (filters.currentMonth + 1).toString(),
}
).toString();
}, [filters]);
const loadData = useCallback(
async (url: string) => {
const data = (await fetch(`${url}?${params}`)).json();
async () => {
const data = (await fetch(`/posts?${params}`)).json();
return data;
},
[filters]
[filters, params]
);
const swr = useSWR(`/posts`, loadData, {
const swr = useSWR(`/posts-${params}`, loadData, {
refreshInterval: 3600000,
refreshWhenOffline: false,
refreshWhenHidden: false,
revalidateOnFocus: false,
});
const setFiltersWrapper = useCallback(
(filters: {
currentWeek: number;
currentYear: number;
currentMonth: number;
display: 'week' | 'month';
}) => {
setFilters(filters);
setInternalData([]);
window.history.replaceState(
null,
'',
`/launches?${
filters.currentWeek
? `week=${filters.currentWeek}`
: `month=${filters.currentMonth}`
}&year=${filters.currentYear}`
);
},
[filters, swr.mutate]
);
const { isLoading } = swr;
const { posts, comments } = swr?.data || { posts: [], comments: [] };
@ -158,6 +176,7 @@ export const CalendarWeekProvider: FC<{
<CalendarContext.Provider
value={{
trendings,
reloadCalendarView: swr.mutate,
...filters,
posts: isLoading ? [] : internalData,
integrations,

View File

@ -1,6 +1,13 @@
'use client';
import React, { FC, useCallback, useMemo } from 'react';
import React, {
FC,
Fragment,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import {
Integrations,
useCalendar,
@ -17,14 +24,16 @@ import { Integration, Post, State } from '@prisma/client';
import { useAddProvider } from '@gitroom/frontend/components/launches/add.provider.component';
import { CommentComponent } from '@gitroom/frontend/components/launches/comments/comment.component';
import { useSWRConfig } from 'swr';
import { useIntersectionObserver } from '@uidotdev/usehooks';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
import { PreviewPopup } from '@gitroom/frontend/components/marketplace/special.message';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
export const days = [
'',
'Monday',
'Tuesday',
'Wednesday',
@ -33,191 +42,155 @@ export const days = [
'Saturday',
'Sunday',
];
export const hours = [
'00:00',
'01:00',
'02:00',
'03:00',
'04:00',
'05:00',
'06:00',
'07:00',
'08:00',
'09:00',
'10:00',
'11:00',
'12:00',
'13:00',
'14:00',
'15:00',
'16:00',
'17:00',
'18:00',
'19:00',
'20:00',
'21:00',
'22:00',
'23:00',
];
export const hours = Array.from({ length: 24 }, (_, i) => i);
export const WeekView = () => {
const { currentYear, currentWeek } = useCalendar();
return (
<div className="flex flex-col h-screen overflow-hidden text-textColor flex-1">
<div className="flex-1">
<div className="grid grid-cols-8 bg-customColor31 gap-[1px] border-customColor31 border rounded-[10px]">
<div className="bg-customColor20 sticky top-0 z-10 bg-gray-900"></div>
{days.map((day, index) => (
<div
key={day}
className="sticky top-0 z-10 bg-customColor20 p-2 text-center"
>
<div>{day}</div>
</div>
))}
{hours.map((hour) => (
<Fragment key={hour}>
<div className="p-2 pr-4 bg-secondary text-center items-center justify-center flex">
{hour.toString().padStart(2, '0')}:00
</div>
{days.map((day, indexDay) => (
<Fragment key={`${day}-${hour}`}>
<div className="relative bg-secondary">
<CalendarColumn
getDate={dayjs()
.year(currentYear)
.week(currentWeek)
.day(indexDay + 1)
.hour(hour)
.startOf('hour')}
/>
</div>
</Fragment>
))}
</Fragment>
))}
</div>
</div>
</div>
);
};
export const MonthView = () => {
const { currentYear, currentMonth } = useCalendar();
const calendarDays = useMemo(() => {
const startOfMonth = dayjs(new Date(currentYear, currentMonth, 1));
// Calculate the day offset for Monday (isoWeekday() returns 1 for Monday)
const startDayOfWeek = startOfMonth.isoWeekday(); // 1 for Monday, 7 for Sunday
const daysBeforeMonth = startDayOfWeek - 1; // Days to show from the previous month
// Get the start date (Monday of the first week that includes this month)
const startDate = startOfMonth.subtract(daysBeforeMonth, 'day');
// Create an array to hold the calendar days (6 weeks * 7 days = 42 days max)
const calendarDays = [];
let currentDay = startDate;
for (let i = 0; i < 42; i++) {
let label = 'current-month';
if (currentDay.month() < currentMonth) label = 'previous-month';
if (currentDay.month() > currentMonth) label = 'next-month';
calendarDays.push({
day: currentDay,
label,
});
// Move to the next day
currentDay = currentDay.add(1, 'day');
}
return calendarDays;
}, [currentYear, currentMonth]);
return (
<div className="flex flex-col h-screen overflow-hidden text-textColor flex-1">
<div className="flex-1 flex">
<div className="grid grid-cols-7 grid-rows-[40px_auto] bg-customColor31 gap-[1px] border-customColor31 border rounded-[10px] flex-1">
{days.map((day) => (
<div
key={day}
className="sticky top-0 z-10 bg-customColor20 p-2 text-center"
>
<div>{day}</div>
</div>
))}
{calendarDays.map((date, index) => (
<div
key={index}
className="bg-secondary text-center items-center justify-center flex min-h-[100px]"
>
<CalendarColumn
getDate={dayjs(date.day).endOf('day')}
randomHour={true}
/>
</div>
))}
</div>
</div>
</div>
);
};
export const Calendar = () => {
const { currentWeek, currentYear, comments } = useCalendar();
const firstDay = useMemo(() => {
return dayjs().year(currentYear).isoWeek(currentWeek).isoWeekday(1);
}, [currentYear, currentWeek]);
const { display } = useCalendar();
return (
<DNDProvider>
<div className="select-none">
<div className="grid grid-cols-8 text-center border-tableBorder border-r">
{days.map((day, index) => (
<div
className="border-tableBorder gap-[4px] border-l border-b h-[36px] border-t flex items-center justify-center bg-input text-[14px] sticky top-0 z-[100]"
key={day}
>
<div>{day} </div>
<div className="text-[12px]">
{day && `(${firstDay.add(index - 1, 'day').format('DD/MM')})`}
</div>
</div>
))}
{hours.map((hour) =>
days.map((day, index) => (
<>
{index === 0 ? (
<div
className="border-tableBorder border-l border-b h-[216px]"
key={day + hour}
>
{['00', '10', '20', '30', '40', '50'].map((num) => (
<div
key={day + hour + num}
className="h-[calc(216px/6)] text-[12px] flex justify-center items-center"
>
{hour.split(':')[0] + ':' + num}
</div>
))}
</div>
) : (
<div
className="group relative border-tableBorder border-l border-b h-[216px] flex flex-col overflow-hidden"
key={day + hour}
>
<CommentBox
totalComments={
comments.find(
(p) =>
dayjs
.utc(p.date)
.local()
.format('YYYY-MM-DD HH:mm') ===
dayjs()
.isoWeek(currentWeek)
.isoWeekday(index + 1)
.hour(+hour.split(':')[0] - 1)
.minute(0)
.format('YYYY-MM-DD HH:mm')
)?.total || 0
}
date={dayjs()
.isoWeek(currentWeek)
.isoWeekday(index + 1)
.hour(+hour.split(':')[0] - 1)
.minute(0)}
/>
{['00', '10', '20', '30', '40', '50'].map((num) => (
<CalendarColumn
key={day + hour + num + currentWeek + currentYear}
day={index}
hour={hour.split(':')[0] + ':' + num}
/>
))}
</div>
)}
</>
))
)}
</div>
</div>
{display === 'week' ? <WeekView /> : <MonthView />}
</DNDProvider>
);
};
export const CalendarColumn: FC<{ day: number; hour: string }> = (props) => {
const { day, hour } = props;
const { currentWeek, currentYear } = useCalendar();
const getDate = useMemo(() => {
const date =
dayjs()
.year(currentYear)
.isoWeek(currentWeek)
.isoWeekday(day)
.format('YYYY-MM-DD') +
'T' +
hour +
':00';
return dayjs(date);
}, [currentWeek]);
const isBeforeNow = useMemo(() => {
return getDate.isBefore(dayjs());
}, [getDate]);
const [ref, entry] = useIntersectionObserver({
threshold: 0.5,
root: null,
rootMargin: '0px',
});
return (
<div className="w-full h-full" ref={ref}>
{!entry?.isIntersecting ? (
<div />
) : (
<CalendarColumnRender {...props} />
)}
</div>
);
};
const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
const { day, hour } = props;
export const CalendarColumn: FC<{
getDate: dayjs.Dayjs;
randomHour?: boolean;
}> = (props) => {
const { getDate, randomHour } = props;
const user = useUser();
const {
currentWeek,
currentYear,
integrations,
posts,
trendings,
changeDate,
display,
reloadCalendarView,
} = useCalendar();
const toaster = useToaster();
const modal = useModals();
const fetch = useFetch();
const getDate = useMemo(() => {
const date =
dayjs()
.year(currentYear)
.isoWeek(currentWeek)
.isoWeekday(day)
.format('YYYY-MM-DD') +
'T' +
hour +
':00';
return dayjs(date);
}, [currentWeek]);
const postList = useMemo(() => {
return posts.filter((post) => {
return dayjs
.utc(post.publishDate)
.local()
.isBetween(getDate, getDate.add(59, 'minute'), 'minute', '[)');
const pList = dayjs.utc(post.publishDate).local();
const check =
display === 'week'
? pList.isSameOrAfter(getDate.startOf('hour')) &&
pList.isBefore(getDate.endOf('hour'))
: pList.format('DD/MM/YYYY') === getDate.format('DD/MM/YYYY');
return check;
});
}, [posts]);
}, [posts, display, getDate]);
const canBeTrending = useMemo(() => {
return !!trendings.find((trend) => {
@ -229,7 +202,7 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
}, [trendings]);
const isBeforeNow = useMemo(() => {
return getDate.isBefore(dayjs());
return getDate.startOf('hour').isBefore(dayjs().startOf('hour'));
}, [getDate]);
const [{ canDrop }, drop] = useDrop(() => ({
@ -311,6 +284,7 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
return previewPublication(post);
}
const data = await (await fetch(`/posts/${post.id}`)).json();
const publishDate = dayjs.utc(data.posts[0].publishDate).local();
modal.openModal({
closeOnClickOutside: false,
@ -323,11 +297,12 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
<ExistingDataContextProvider value={data}>
<AddEditModal
reopenModal={editPost(post)}
mutate={reloadCalendarView}
integrations={integrations
.slice(0)
.filter((f) => f.id === data.integration)
.map((p) => ({ ...p, picture: data.integrationPicture }))}
date={getDate}
date={publishDate}
/>
</ExistingDataContextProvider>
),
@ -349,84 +324,103 @@ const CalendarColumnRender: FC<{ day: number; hour: string }> = (props) => {
children: (
<AddEditModal
integrations={integrations.slice(0).map((p) => ({ ...p }))}
date={getDate}
mutate={reloadCalendarView}
date={
randomHour ? getDate.hour(Math.floor(Math.random() * 24)) : getDate
}
reopenModal={() => ({})}
/>
),
size: '80%',
// title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`,
});
}, [integrations]);
}, [integrations, getDate]);
const addProvider = useAddProvider();
return (
<div className="relative flex flex-col w-full min-h-full">
<div className="flex flex-col w-full min-h-full" ref={drop}>
{display === 'month' && (
<div className={clsx('pt-[5px]', isBeforeNow && 'bg-customColor23')}>
{getDate.date()}
</div>
)}
<div
{...(canBeTrending
? {
'data-tooltip-id': 'tooltip',
'data-tooltip-content': 'Predicted GitHub Trending Change',
}
: {})}
ref={drop}
className={clsx(
'flex-col flex-1 text-[12px] pointer w-full overflow-hidden justify-center overflow-x-auto flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
isBeforeNow && 'bg-customColor23',
canDrop && 'bg-white/80',
canBeTrending && 'bg-customColor24'
'relative flex flex-col flex-1',
canDrop && 'bg-white/80'
)}
>
{postList.map((post) => (
<div
{...(canBeTrending
? {
'data-tooltip-id': 'tooltip',
'data-tooltip-content': 'Predicted GitHub Trending Change',
}
: {})}
className={clsx(
'flex-col text-[12px] pointer w-full cursor-pointer overflow-hidden overflow-x-auto flex scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary',
isBeforeNow && 'bg-customColor23 flex-1',
canBeTrending && 'bg-customColor24'
)}
>
{postList.map((post) => (
<div
key={post.id}
className={clsx(
'text-textColor p-[2.5px] relative flex flex-col justify-center items-center'
)}
>
<div className="relative w-full flex flex-col items-center p-[2.5px]">
<CalendarItem
isBeforeNow={isBeforeNow}
date={getDate}
state={post.state}
editPost={editPost(post)}
post={post}
integrations={integrations}
/>
</div>
</div>
))}
</div>
{!isBeforeNow && (
<div
key={post.id}
className={clsx(
'text-textColor p-[2.5px] relative flex flex-col justify-center items-center'
)}
className="pb-[2.5px] px-[5px] flex-1 flex"
onClick={integrations.length ? addModal : addProvider}
>
<div className="relative w-full flex flex-col items-center p-[2.5px]">
<CalendarItem
date={getDate}
state={post.state}
editPost={editPost(post)}
post={post}
integrations={integrations}
<div
className={clsx(
display === 'month'
? 'flex-1 min-h-[40px] w-full'
: !postList.length
? 'h-full w-full absolute left-0 top-0 p-[5px]'
: 'min-h-[40px] w-full',
'flex items-center justify-center cursor-pointer pb-[2.5px]'
)}
>
<div
className={clsx(
'hover:before:content-["+"] w-full h-full text-seventh rounded-[10px] hover:border hover:border-seventh flex justify-center items-center'
)}
/>
</div>
</div>
))}
)}
</div>
{!isBeforeNow && (
<div className="pb-[2.5px] px-[5px]">
<div
className={clsx(
!postList.length
? 'h-full w-full absolute left-0 top-0 p-[5px]'
: 'h-[40px]',
'flex items-center justify-center cursor-pointer pb-[2.5px]'
)}
>
<div
onClick={integrations.length ? addModal : addProvider}
className={clsx(
'hover:before:content-["+"] w-full h-full text-seventh rounded-[10px] hover:border hover:border-seventh flex justify-center items-center'
)}
/>
</div>
</div>
)}
</div>
);
};
const CalendarItem: FC<{
date: dayjs.Dayjs;
isBeforeNow: boolean;
editPost: () => void;
integrations: Integrations[];
state: State;
post: Post & { integration: Integration };
}> = (props) => {
const { editPost, post, date, integrations, state } = props;
const { editPost, post, date, isBeforeNow, state } = props;
const [{ opacity }, dragRef] = useDrag(
() => ({
type: 'post',
@ -444,7 +438,7 @@ const CalendarItem: FC<{
className={clsx(
'gap-[5px] w-full flex h-full flex-1 rounded-[10px] border border-seventh px-[5px] p-[2.5px]',
'relative',
state === 'DRAFT' && '!grayscale'
(state === 'DRAFT' || isBeforeNow) && '!grayscale'
)}
style={{ opacity }}
>
@ -458,7 +452,10 @@ const CalendarItem: FC<{
src={`/icons/platforms/${post.integration?.providerIdentifier}.png`}
/>
</div>
<div className="whitespace-pre-wrap line-clamp-3">{post.content}</div>
<div className="whitespace-pre-wrap line-clamp-3">
{state === 'DRAFT' ? 'Draft: ' : ''}
{post.content}
</div>
</div>
);
};

View File

@ -1,32 +1,109 @@
'use client';
import { useCalendar } from '@gitroom/frontend/components/launches/calendar.context';
import clsx from 'clsx';
import dayjs from 'dayjs';
import {useCallback} from "react";
import { useCallback } from 'react';
export const Filters = () => {
const week = useCalendar();
const betweenDates =
dayjs().year(week.currentYear).isoWeek(week.currentWeek).startOf('isoWeek').format('DD/MM/YYYY') +
' - ' +
dayjs().year(week.currentYear).isoWeek(week.currentWeek).endOf('isoWeek').format('DD/MM/YYYY');
week.display === 'week'
? dayjs()
.year(week.currentYear)
.isoWeek(week.currentWeek)
.startOf('isoWeek')
.format('DD/MM/YYYY') +
' - ' +
dayjs()
.year(week.currentYear)
.isoWeek(week.currentWeek)
.endOf('isoWeek')
.format('DD/MM/YYYY')
: dayjs()
.year(week.currentYear)
.month(week.currentMonth)
.startOf('month')
.format('DD/MM/YYYY') +
' - ' +
dayjs()
.year(week.currentYear)
.month(week.currentMonth)
.endOf('month')
.format('DD/MM/YYYY');
const nextWeek = useCallback(() => {
week.setFilters({
currentWeek: week.currentWeek === 52 ? 1 : week.currentWeek + 1,
currentYear: week.currentWeek === 52 ? week.currentYear + 1 : week.currentYear,
});
}, [week.currentWeek, week.currentYear]);
const setWeek = useCallback(() => {
week.setFilters({
currentWeek: dayjs().isoWeek(),
currentYear: dayjs().year(),
currentMonth: 0,
display: 'week',
});
}, [week]);
const previousWeek = useCallback(() => {
week.setFilters({
currentWeek: week.currentWeek === 1 ? 52 : week.currentWeek - 1,
currentYear: week.currentWeek === 1 ? week.currentYear - 1 : week.currentYear,
});
}, [week.currentWeek, week.currentYear]);
const setMonth = useCallback(() => {
week.setFilters({
currentMonth: dayjs().month(),
currentWeek: 0,
currentYear: dayjs().year(),
display: 'month',
});
}, [week]);
const next = useCallback(() => {
week.setFilters({
currentWeek:
week.display === 'week'
? week.currentWeek === 52
? 1
: week.currentWeek + 1
: 0,
currentYear:
week.display === 'week'
? week.currentWeek === 52
? week.currentYear + 1
: week.currentYear
: week.currentMonth === 11
? week.currentYear + 1
: week.currentYear,
display: week.display as any,
currentMonth:
week.display === 'week'
? 0
: week.currentMonth === 11
? 0
: week.currentMonth + 1,
});
}, [week.display, week.currentMonth, week.currentWeek, week.currentYear]);
const previous = useCallback(() => {
week.setFilters({
currentWeek:
week.display === 'week'
? week.currentWeek === 1
? 52
: week.currentWeek - 1
: 0,
currentYear:
week.display === 'week'
? week.currentWeek === 1
? week.currentYear - 1
: week.currentYear
: week.currentMonth === 0
? week.currentYear - 1
: week.currentYear,
display: week.display as any,
currentMonth:
week.display === 'week'
? 0
: week.currentMonth === 0
? 11
: week.currentMonth - 1,
});
}, [week.display, week.currentMonth, week.currentWeek, week.currentYear]);
return (
<div className="h-[20px] text-textColor flex gap-[8px] items-center select-none">
<div onClick={previousWeek}>
<div className="text-textColor flex gap-[8px] items-center select-none">
<div onClick={previous}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
@ -40,8 +117,12 @@ export const Filters = () => {
/>
</svg>
</div>
<div>Week {week.currentWeek}</div>
<div onClick={nextWeek}>
<div className="w-[80px] text-center">
{week.display === 'week'
? `Week ${week.currentWeek}`
: `${dayjs().month(week.currentMonth).format('MMMM')}`}
</div>
<div onClick={next}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
@ -55,7 +136,25 @@ export const Filters = () => {
/>
</svg>
</div>
<div>{betweenDates}</div>
<div className="flex-1">{betweenDates}</div>
<div
className={clsx(
'border border-tableBorder p-[10px]',
week.display === 'week' && 'bg-tableBorder'
)}
onClick={setWeek}
>
Week
</div>
<div
className={clsx(
'border border-tableBorder p-[10px]',
week.display === 'month' && 'bg-tableBorder'
)}
onClick={setMonth}
>
Month
</div>
</div>
);
};

View File

@ -13,13 +13,12 @@ import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
import clsx from 'clsx';
import { useUser } from '../layout/user.context';
import { Menu } from '@gitroom/frontend/components/launches/menu/menu';
import { GeneratorComponent } from '@gitroom/frontend/components/launches/generator/generator';
import { useRouter, useSearchParams } from 'next/navigation';
import { Integration } from '@prisma/client';
import ImageWithFallback from '@gitroom/react/helpers/image.with.fallback';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
import { NewCalendarComponent } from '@gitroom/frontend/components/launches/new.calendar.component';
import { Calendar } from './calendar';
export const LaunchesComponent = () => {
const fetch = useFetch();
@ -117,7 +116,7 @@ export const LaunchesComponent = () => {
<CalendarWeekProvider integrations={sortedIntegrations}>
<div className="flex flex-1 flex-col">
<div className="flex flex-1 relative">
<div className="outline-none absolute w-full h-full grid grid-cols-[220px_minmax(0,1fr)] gap-[30px] overflow-hidden overflow-y-auto scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary">
<div className="outline-none w-full h-full grid grid-cols-[220px_minmax(0,1fr)] gap-[30px] scrollbar scrollbar-thumb-tableBorder scrollbar-track-secondary">
<div className="w-[220px] bg-third p-[16px] flex flex-col gap-[24px] min-h-[100%]">
<h2 className="text-[20px]">Channels</h2>
<div className="gap-[16px] flex flex-col">
@ -213,8 +212,7 @@ export const LaunchesComponent = () => {
</div>
<div className="flex-1 flex flex-col gap-[14px]">
<Filters />
<NewCalendarComponent />
{/*<Calendar />*/}
<Calendar />
</div>
</div>
</div>

View File

@ -1,56 +0,0 @@
'use client';
import { ChevronLeft, ChevronRight, Plus } from 'lucide-react';
import { Button } from '@gitroom/react/form/button';
import { Fragment } from 'react';
import { CalendarColumn } from '@gitroom/frontend/components/launches/calendar';
import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.provider';
export const days = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
export const hours = Array.from({ length: 24 }, (_, i) => i);
export const NewCalendarComponent = () => {
return (
<DNDProvider>
<div className="flex flex-col h-screen overflow-hidden text-textColor flex-1">
<div className="flex-1">
<div className="grid grid-cols-8 bg-customColor31 gap-[1px] border-customColor31 border rounded-[10px]">
<div className="bg-customColor20 sticky top-0 z-10 bg-gray-900"></div>
{days.map((day, index) => (
<div
key={day}
className="sticky top-0 z-10 bg-customColor20 p-2 text-center"
>
<div>{day}</div>
</div>
))}
{hours.map((hour) => (
<Fragment key={hour}>
<div className="p-2 pr-4 bg-secondary text-center items-center justify-center flex">
{hour.toString().padStart(2, '0')}:00
</div>
{days.map((day, indexDay) => (
<Fragment key={`${day}-${hour}`}>
<div className="relative bg-secondary">
<CalendarColumn
day={indexDay}
hour={`${hour.toString().padStart(2, '0')}:00`}
/>
</div>
</Fragment>
))}
</Fragment>
))}
</div>
</div>
</div>
</DNDProvider>
);
};

View File

@ -149,6 +149,7 @@ module.exports = {
}),
screens: {
custom: { raw: '(max-height: 800px)' },
xs: { max: '401px'} ,
},
},
},

View File

@ -1,26 +1,40 @@
version: '3.9'
services:
gitroom-postgres:
postiz-postgres:
image: postgres:14.5
container_name: gitroom-postgres
container_name: postiz-postgres
restart: always
environment:
POSTGRES_PASSWORD: gitroom-local-pwd
POSTGRES_USER: gitroom-local
POSTGRES_DB: gitroom-db-local
POSTGRES_PASSWORD: postiz-local-pwd
POSTGRES_USER: postiz-local
POSTGRES_DB: postiz-db-local
volumes:
- postgres-volume:/var/lib/postgresql/data
ports:
- 5432:5432
gitroom-redis:
networks:
- postiz-network
postiz-pg-admin:
image: dpage/pgadmin4
container_name: postiz-pg-admin
restart: always
ports:
- 8081:80
environment:
PGADMIN_DEFAULT_EMAIL: admin@admin.com
PGADMIN_DEFAULT_PASSWORD: admin
networks:
- postiz-network
postiz-redis:
image: redis:7.2
container_name: gitroom-redis
container_name: postiz-redis
restart: always
ports:
- 6379:6379
volumes:
postgres-volume:
external: false
networks:
postiz-network:
external: false

View File

@ -116,10 +116,9 @@ export class CommentsRepository {
orgId: string,
year: number,
week: number,
isIsoWeek: boolean
) {
const dateYear = dayjs().year(year);
const date = isIsoWeek ? dateYear.isoWeek(week) : dateYear.week(week);
const date = dateYear.isoWeek(week);
const startDate = date.startOf('isoWeek').subtract(2, 'days').toDate();
const endDate = date.endOf('isoWeek').add(2, 'days').toDate();

View File

@ -44,7 +44,7 @@ export class CommentsService {
);
}
getAllCommentsByWeekYear(orgId: string, year: number, week: number, isIsoWeek: boolean) {
return this._commentsRepository.getAllCommentsByWeekYear(orgId, year, week, isIsoWeek);
getAllCommentsByWeekYear(orgId: string, year: number, week: number) {
return this._commentsRepository.getAllCommentsByWeekYear(orgId, year, week);
}
}

View File

@ -64,10 +64,10 @@ export class PostsRepository {
getPosts(orgId: string, query: GetPostsDto) {
const dateYear = dayjs().year(query.year);
const date = query.isIsoWeek === 'true' ? dateYear.isoWeek(query.week) : dateYear.week(query.week);
const date = query.week ? dateYear.isoWeek(query.week) : dateYear.month(query.month-1);
const startDate = date.startOf('week').subtract(2, 'days').toDate();
const endDate = date.endOf('week').add(2, 'days').toDate();
const startDate = (query.week ? date.startOf('isoWeek') : date.startOf('month')).subtract(2, 'days').toDate();
const endDate = (query.week ? date.endOf('isoWeek') : date.endOf('month')).add(2, 'days').toDate();
return this._post.model.post.findMany({
where: {

View File

@ -1,21 +1,25 @@
import { Type } from 'class-transformer';
import { IsIn, IsNumber, IsString, Max, Min } from 'class-validator';
import { IsIn, IsNumber, IsString, Max, Min, ValidateIf } from 'class-validator';
import dayjs from 'dayjs';
export class GetPostsDto {
@ValidateIf((o) => !o.month)
@Type(() => Number)
@IsNumber()
@Max(52)
@Min(1)
week: number;
@ValidateIf((o) => !o.week)
@Type(() => Number)
@IsNumber()
@Max(52)
@Min(1)
month: number;
@Type(() => Number)
@IsNumber()
@Max(dayjs().add(10, 'year').year())
@Min(2022)
year: number;
@IsIn(['true', 'false'])
@IsString()
isIsoWeek: 'true' | 'false';
}

View File

@ -8,7 +8,7 @@
"scripts": {
"dev": "npx nx run-many --target=serve --projects=frontend,backend,workers --parallel=4",
"dev:stripe": "npx concurrently \"stripe listen --forward-to localhost:3000/stripe\" \"npm run dev\"",
"build": "npx nx run-many --target=build --projects=frontend,backend,workers",
"build": "npx nx run-many --target=build --projects=frontend,backend,workers,cron",
"start:prod": "node dist/apps/backend/main.js",
"start:prod:frontend": "nx run frontend:serve:production",
"start:prod:workers": "node dist/apps/workers/main.js",
@ -20,6 +20,8 @@
"prisma-generate": "cd ./libraries/nestjs-libraries/src/database/prisma && npx prisma generate",
"prisma-db-push": "cd ./libraries/nestjs-libraries/src/database/prisma && npx prisma db push",
"prisma-reset": "cd ./libraries/nestjs-libraries/src/database/prisma && npx prisma db push --force-reset && npx prisma db push",
"docker-build": "./var/docker/docker-build.sh",
"docker-create": "./var/docker/docker-create.sh",
"postinstall": "npm run prisma-generate"
},
"private": true,

7
var/docker/docker-build.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
set -o xtrace
docker rmi localhost/postiz || true
docker build --target dist -t localhost/postiz -f Dockerfile.dev .
docker build --target devcontainer -t localhost/postiz-devcontainer -f Dockerfile.dev .

5
var/docker/docker-create.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
docker kill postiz || true
docker rm postiz || true
docker create --name postiz -p 3000:3000 -p 4200:4200 localhost/postiz

36
var/docker/entrypoint.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
if [[ "$SKIP_CONFIG_CHECK" != "true" ]]; then
echo "symlinking /config/.env into /app/.env"
if [ ! -f /config/.env ]; then
echo "ERROR: No .env file found in /config/.env"
fi
ln -sf /config/.env /app/.env
fi
if [[ "$POSTIZ_APPS" -eq "" ]]; then
echo "POSTIZ_APPS is not set, starting everything!"
POSTIZ_APPS="frontend workers cron backend"
fi
mkdir -p /etc/supervisor.d/
if [[ "$POSTIZ_APPS" == *"frontend"* ]]; then
ln -sf /app/supervisord_available_configs/frontend.conf /etc/supervisor.d/
fi
if [[ $POSTIZ_APPS == *"workers"* ]]; then
ln -sf /app/supervisord_available_configs/workers.conf /etc/supervisor.d/
fi
if [[ $POSTIZ_APPS == *"cron"* ]]; then
ln -sf /app/supervisord_available_configs/cron.conf /etc/supervisor.d/
fi
if [[ $POSTIZ_APPS == *"backend"* ]]; then
ln -sf /app/supervisord_available_configs/backend.conf /etc/supervisor.d/
fi
/usr/bin/supervisord

View File

@ -0,0 +1,16 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
[unix_http_server]
file=/run/supervisord.sock
[include]
files = /etc/supervisor.d/*.conf
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///run/supervisord.sock

View File

@ -0,0 +1,8 @@
[program:backend]
directory=/app
command=npm run start:prod
autostart=true
autorestart=false
redirect_stderr=true
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0

View File

@ -0,0 +1,8 @@
[program:cron]
directory=/app
command=npm run start:prod:cron
autostart=true
autorestart=false
redirect_stderr=true
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0

View File

@ -0,0 +1,8 @@
[program:frontend]
directory=/app
command=npm run start:prod:frontend
autostart=true
autorestart=false
redirect_stderr=true
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0

View File

@ -0,0 +1,8 @@
[program:workers]
directory=/app
command=npm run start:prod:workers
autostart=true
autorestart=false
redirect_stderr=true
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0