diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts
index 3bbabf7d..bd14422e 100644
--- a/apps/backend/src/api/routes/auth.controller.ts
+++ b/apps/backend/src/api/routes/auth.controller.ts
@@ -6,8 +6,8 @@ import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto
import { AuthService } from '@gitroom/backend/services/auth/auth.service';
import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto';
import { ForgotPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot.password.dto';
-import { removeSubdomain } from '@gitroom/helpers/subdomain/subdomain.management';
import { ApiTags } from '@nestjs/swagger';
+import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
@ApiTags('Auth')
@Controller('/auth')
@@ -37,8 +37,7 @@ export class AuthController {
}
response.cookie('auth', jwt, {
- domain:
- '.' + new URL(removeSubdomain(process.env.FRONTEND_URL!)).hostname,
+ domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
@@ -47,8 +46,7 @@ export class AuthController {
if (typeof addedOrg !== 'boolean' && addedOrg?.organizationId) {
response.cookie('showorg', addedOrg.organizationId, {
- domain:
- '.' + new URL(removeSubdomain(process.env.FRONTEND_URL!)).hostname,
+ domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
@@ -83,8 +81,7 @@ export class AuthController {
);
response.cookie('auth', jwt, {
- domain:
- '.' + new URL(removeSubdomain(process.env.FRONTEND_URL!)).hostname,
+ domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
@@ -93,8 +90,7 @@ export class AuthController {
if (typeof addedOrg !== 'boolean' && addedOrg?.organizationId) {
response.cookie('showorg', addedOrg.organizationId, {
- domain:
- '.' + new URL(removeSubdomain(process.env.FRONTEND_URL!)).hostname,
+ domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
@@ -149,8 +145,7 @@ export class AuthController {
}
response.cookie('auth', activate, {
- domain:
- '.' + new URL(removeSubdomain(process.env.FRONTEND_URL!)).hostname,
+ domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
@@ -173,8 +168,7 @@ export class AuthController {
}
response.cookie('auth', jwt, {
- domain:
- '.' + new URL(removeSubdomain(process.env.FRONTEND_URL!)).hostname,
+ domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts
index c13c614a..dccdd28c 100644
--- a/apps/backend/src/api/routes/posts.controller.ts
+++ b/apps/backend/src/api/routes/posts.controller.ts
@@ -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,
};
}
diff --git a/apps/backend/src/services/auth/auth.middleware.ts b/apps/backend/src/services/auth/auth.middleware.ts
index a54dfb4a..a10a7e12 100644
--- a/apps/backend/src/services/auth/auth.middleware.ts
+++ b/apps/backend/src/services/auth/auth.middleware.ts
@@ -4,12 +4,12 @@ import { AuthService } from '@gitroom/helpers/auth/auth.service';
import { User } from '@prisma/client';
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
-import { removeSubdomain } from '@gitroom/helpers/subdomain/subdomain.management';
+import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
export const removeAuth = (res: Response) => {
res.cookie('auth', '', {
- domain: '.' + new URL(removeSubdomain(process.env.FRONTEND_URL!)).hostname,
+ domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
diff --git a/apps/docs/emails.mdx b/apps/docs/emails.mdx
index 1e45b704..e7c7ea45 100644
--- a/apps/docs/emails.mdx
+++ b/apps/docs/emails.mdx
@@ -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=""
```
-Feel free to contribute other providers to send email notifications.
\ No newline at end of file
+Feel free to contribute other providers to send email notifications.
diff --git a/apps/docs/howitworks.mdx b/apps/docs/howitworks.mdx
index 8232d9d8..aa1fa7dc 100644
--- a/apps/docs/howitworks.mdx
+++ b/apps/docs/howitworks.mdx
@@ -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.
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.
-**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!
-If you don't, you can install [Docker](https://www.docker.com/products/docker-desktop) and run:
+Make sure you have PostgreSQL installed on your machine.
-```bash
+#### 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
-```
-
-### Redis
-
-Make sure you have Redis installed on your machine.
-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
+#### 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
@@ -44,7 +52,7 @@ git clone https://github.com/gitroomhq/gitroom
```
-
+
Copy the `.env.example` file to `.env` and fill in the values
```bash .env
@@ -77,24 +85,16 @@ IS_GENERAL="true" # required for now
-
```bash Terminal
npm install
```
-
-{' '}
-
- Using this you can skip the redis and postgres steps from above. This will
- also give you pg-admin to check the database. ```bash Terminal docker compose
- -f "docker-compose.dev.yaml" up ```
-
-
-{' '}
-
- ```bash Terminal npm run prisma-db-push ```
-
+
+```bash Terminal
+npm run prisma-db-push
+```
+
```bash Terminal
diff --git a/apps/frontend/src/app/colors.scss b/apps/frontend/src/app/colors.scss
index 0a3cf3ca..961de950 100644
--- a/apps/frontend/src/app/colors.scss
+++ b/apps/frontend/src/app/colors.scss
@@ -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;
diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx
index 6a77d17e..7b4912c4 100644
--- a/apps/frontend/src/app/layout.tsx
+++ b/apps/frontend/src/app/layout.tsx
@@ -1,18 +1,14 @@
import interClass from '@gitroom/react/helpers/inter.font';
-
export const dynamic = 'force-dynamic';
import './global.scss';
import 'react-tooltip/dist/react-tooltip.css';
import '@copilotkit/react-ui/styles.css';
-
import LayoutContext from '@gitroom/frontend/components/layout/layout.context';
import { ReactNode } from 'react';
-import { Chakra_Petch } from 'next/font/google';
import { isGeneral } from '@gitroom/react/helpers/is.general';
import PlausibleProvider from 'next-plausible';
import clsx from 'clsx';
-
-const chakra = Chakra_Petch({ weight: '400', subsets: ['latin'] });
+import "@fontsource/chakra-petch";
export default async function AppLayout({ children }: { children: ReactNode }) {
return (
@@ -24,7 +20,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
sizes="any"
/>
-
+
{children}
diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx
index f54a97c5..b4de0725 100644
--- a/apps/frontend/src/components/launches/add.edit.model.tsx
+++ b/apps/frontend/src/components/launches/add.edit.model.tsx
@@ -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'
diff --git a/apps/frontend/src/components/launches/calendar.context.tsx b/apps/frontend/src/components/launches/calendar.context.tsx
index 0105bda9..ae63d017 100644
--- a/apps/frontend/src/components/launches/calendar.context.tsx
+++ b/apps/frontend/src/components/launches/calendar.context.tsx
@@ -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,
- 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([]);
- const { mutate } = useSWRConfig();
+ const [trendings] = useState([]);
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<{
i);
+
+export const WeekView = () => {
+ const { currentYear, currentWeek } = useCalendar();
+
+ return (
+
+
+
+
+ {days.map((day, index) => (
+
+
{day}
+
+ ))}
+ {hours.map((hour) => (
+
+
+ {hour.toString().padStart(2, '0')}:00
+
+ {days.map((day, indexDay) => (
+
+
+
+
+
+ ))}
+
+ ))}
+
+
+
+ );
+};
+
+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 (
+