diff --git a/apps/docs/content/docs/plugins/database.mdx b/apps/docs/content/docs/plugins/database.mdx new file mode 100644 index 000000000..6c5d93bc9 --- /dev/null +++ b/apps/docs/content/docs/plugins/database.mdx @@ -0,0 +1,23 @@ +--- +title: Working with Database +description: elo123 +--- + +VitNode plugins can interact with databases using [Drizzle ORM](https://orm.drizzle.team/) and [PostgreSQL](https://www.postgresql.org/). + +## Schema + +You can define your database schema in `database` directory of your plugin. + +```ts title="plugins/{plugin_name}/src/database/categories.ts" +import { pgTable } from 'drizzle-orm/pg-core'; + +export const blog_categories = pgTable('blog_categories', t => ({ + id: t.serial().primaryKey(), + createdAt: t.timestamp().notNull().defaultNow(), + updatedAt: t + .timestamp() + .notNull() + .$onUpdate(() => new Date()), +})); +``` diff --git a/apps/docs/content/docs/plugins/layouts-and-pages.mdx b/apps/docs/content/docs/plugins/layouts-and-pages.mdx index 8ba17797f..434e02125 100644 --- a/apps/docs/content/docs/plugins/layouts-and-pages.mdx +++ b/apps/docs/content/docs/plugins/layouts-and-pages.mdx @@ -3,85 +3,245 @@ title: Layouts and Pages description: Learn how to create custom layouts and pages for your VitNode plugins --- -VitNode plugins follow the same patterns as Next.js for layouts and pages. Check out the [Next.js documentation](https://nextjs.org/docs/app/getting-started/layouts-and-pages) for detailed information. +VitNode plugins use the same file-system based router as Next.js App Router. This means you can create pages and layouts using the same patterns you're familiar with from Next.js development. - Next.js patterns for **Layouts and Pages** work only within the `app` and - `app_admin` directories. Our watch mode will **automatically copy files** from - your plugin to the root of the VitNode application, allowing you to create - custom pages and layouts for your plugins. + VitNode's watch mode automatically copies files from your plugin's `app` and + `app_admin` directories to the root application, enabling seamless integration + with Next.js App Router patterns. -## Frontend Pages +## Pages -Create pages in the `src/app` directory with a `page.tsx` file: +Pages are React components exported from `page.tsx` files. They are the UI that is unique to a specific route. -```tsx title="plugins/{plugin_name}/src/app/welcome/page.tsx" -export default function Page() { - return

Hello VitNode!

; +### Creating Pages + +Create a page by adding a `page.tsx` file inside the `src/app` directory: + +```tsx title="plugins/{plugin_name}/src/app/page.tsx" +export default function HomePage() { + return

Hello from Plugin Homepage!

; } ``` -This creates a page at `http://localhost:3000/welcome`. +This creates a page accessible at the root of your plugin's route. -### AdminCP Pages +### Nested Routes -Create admin pages in the `src/app_admin` directory: +Create nested routes by creating folders. Each folder represents a route segment: -```tsx title="plugins/{plugin_name}/src/app_admin/welcome/page.tsx" -export default function Page() { - return

Hello VitNode AdminCP!

; +```tsx title="plugins/{plugin_name}/src/app/dashboard/page.tsx" +export default function DashboardPage() { + return

Dashboard Page

; } ``` -This creates an admin page at `http://localhost:3000/admin/welcome`. +```tsx title="plugins/{plugin_name}/src/app/dashboard/settings/page.tsx" +export default function SettingsPage() { + return

Settings Page

; +} +``` - - All pages in the `app_admin` directory are protected by default. - +This creates: + +- `/dashboard` - Dashboard page +- `/dashboard/settings` - Settings page + +### Dynamic Routes + +Create dynamic routes using square brackets `[folderName]`: + +```tsx title="plugins/{plugin_name}/src/app/posts/[id]/page.tsx" +export default async function PostPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( +
+

Post ID: {id}

+
+ ); +} +``` + +### Catch-all Routes + +Create catch-all routes using `[...folderName]`: + +```tsx title="plugins/{plugin_name}/src/app/docs/[...slug]/page.tsx" +export default async function DocsPage({ + params, +}: { + params: Promise<{ slug: string[] }>; +}) { + const { slug } = await params; + + return ( +
+

Docs: {slug.join('/')}

+
+ ); +} +``` ## Layouts -Create shared layouts using `layout.tsx` files, just like in Next.js App Router. +Layouts are UI that is shared between multiple pages. They preserve state, remain interactive, and do not re-render. -### Frontend Layouts +### Root Layout -Create layouts in the `src/app` directory: +Create a root layout for your plugin: -```tsx title="plugins/{plugin_name}/src/app/welcome/layout.tsx" -export default function WelcomeLayout({ +```tsx title="plugins/{plugin_name}/src/app/layout.tsx" +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'My Plugin', + description: 'Welcome to my VitNode plugin', +}; + +export default function PluginLayout({ children, }: { children: React.ReactNode; }) { return ( -
- +
+
+

My Plugin Header

+
{children}
+
+

Plugin Footer

+
); } ``` -This layout will wrap all pages under the `/welcome` route. - -### AdminCP Layouts +### Nested Layouts -Create admin layouts in the `src/app_admin` directory: +Create nested layouts for specific route segments: -```tsx title="plugins/{plugin_name}/src/app_admin/welcome/layout.tsx" -export default function AdminWelcomeLayout({ +```tsx title="plugins/{plugin_name}/src/app/dashboard/layout.tsx" +export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { + return ( +
+ +
{children}
+
+ ); +} +``` + +## AdminCP Pages and Layouts + +AdminCP follows the same patterns but uses the `src/app_admin` directory. + +### Pages + +```tsx title="plugins/{plugin_name}/src/app_admin/page.tsx" +export default function AdminHomePage() { return (
- -
{children}
+

Plugin Admin Dashboard

+

Manage your plugin settings here.

+
+ ); +} +``` + +### Layouts + +```tsx title="plugins/{plugin_name}/src/app_admin/layout.tsx" +export default function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+ +
{children}
); } ``` -This layout will wrap all admin pages under the `/admin/welcome` route. + + All pages in the `app_admin` directory are automatically protected and require + admin authentication. + + +## Metadata + +Add metadata to your pages using the Metadata API: + +```tsx title="plugins/{plugin_name}/src/app/posts/[id]/page.tsx" +import { Metadata } from 'next'; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ id: string }>; +}): Promise { + const { id } = await params; + + return { + title: `Post ${id}`, + description: `Details for post ${id}`, + }; +} + +export default async function PostPage({ params }: PageProps) { + const { id } = await params; + + return

Post: {id}

; +} +``` + +For more advanced patterns and features, check out the [Next.js App Router documentation - Metadata](https://nextjs.org/docs/app/getting-started/metadata-and-og-images). + +## API Reference + + + + + diff --git a/apps/docs/content/docs/plugins/meta.json b/apps/docs/content/docs/plugins/meta.json index 3598ad54b..1ec517051 100644 --- a/apps/docs/content/docs/plugins/meta.json +++ b/apps/docs/content/docs/plugins/meta.json @@ -5,10 +5,12 @@ "root": true, "pages": [ "index", - "---Plugins---", + "---Plugins by VitNode---", "blog", "---Custom Plugin---", "create", - "..." + "...", + "---Database---", + "database" ] } diff --git a/apps/web/components.json b/apps/web/components.json index ffe928f5b..c6a338228 100644 --- a/apps/web/components.json +++ b/apps/web/components.json @@ -5,7 +5,7 @@ "tsx": true, "tailwind": { "config": "", - "css": "src/app/globals.css", + "css": "src/app/global.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/apps/web/global.d.ts b/apps/web/global.d.ts deleted file mode 100644 index 5c652b297..000000000 --- a/apps/web/global.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type core from '@/plugins/core/langs/en.json'; - -type Messages = typeof core; - -declare module 'next-intl' { - interface AppConfig { - Messages: Messages; - } -} diff --git a/apps/web/migrations/0001_material_elektra.sql b/apps/web/migrations/0001_material_elektra.sql new file mode 100644 index 000000000..50eca6e96 --- /dev/null +++ b/apps/web/migrations/0001_material_elektra.sql @@ -0,0 +1,7 @@ +CREATE TABLE "blog_categories" ( + "id" serial PRIMARY KEY NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp NOT NULL +); +--> statement-breakpoint +ALTER TABLE "blog_categories" ENABLE ROW LEVEL SECURITY; \ No newline at end of file diff --git a/apps/web/migrations/meta/0001_snapshot.json b/apps/web/migrations/meta/0001_snapshot.json new file mode 100644 index 000000000..1732f91bb --- /dev/null +++ b/apps/web/migrations/meta/0001_snapshot.json @@ -0,0 +1,1257 @@ +{ + "id": "cd2662f4-b267-412f-8c81-0808254010d2", + "prevId": "b2e3a2e7-f9c3-449a-92a9-8010647fe22e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.core_admin_permissions": { + "name": "core_admin_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "core_admin_permissions_role_id_idx": { + "name": "core_admin_permissions_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_admin_permissions_user_id_idx": { + "name": "core_admin_permissions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_admin_permissions_roleId_core_roles_id_fk": { + "name": "core_admin_permissions_roleId_core_roles_id_fk", + "tableFrom": "core_admin_permissions", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_admin_permissions_userId_core_users_id_fk": { + "name": "core_admin_permissions_userId_core_users_id_fk", + "tableFrom": "core_admin_permissions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_admin_sessions": { + "name": "core_admin_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lastSeen": { + "name": "lastSeen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deviceId": { + "name": "deviceId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_admin_sessions_token_idx": { + "name": "core_admin_sessions_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_admin_sessions_user_id_idx": { + "name": "core_admin_sessions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_admin_sessions_userId_core_users_id_fk": { + "name": "core_admin_sessions_userId_core_users_id_fk", + "tableFrom": "core_admin_sessions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk": { + "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", + "tableFrom": "core_admin_sessions", + "tableTo": "core_sessions_known_devices", + "columnsFrom": [ + "deviceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_admin_sessions_token_unique": { + "name": "core_admin_sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_languages": { + "name": "core_languages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "time24": { + "name": "time24", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "core_languages_code_idx": { + "name": "core_languages_code_idx", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_languages_name_idx": { + "name": "core_languages_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_languages_code_unique": { + "name": "core_languages_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_languages_words": { + "name": "core_languages_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "languageCode": { + "name": "languageCode", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "pluginCode": { + "name": "pluginCode", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "itemId": { + "name": "itemId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tableName": { + "name": "tableName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "variable": { + "name": "variable", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_languages_words_lang_code_idx": { + "name": "core_languages_words_lang_code_idx", + "columns": [ + { + "expression": "languageCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_languages_words_languageCode_core_languages_code_fk": { + "name": "core_languages_words_languageCode_core_languages_code_fk", + "tableFrom": "core_languages_words", + "tableTo": "core_languages", + "columnsFrom": [ + "languageCode" + ], + "columnsTo": [ + "code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_moderators_permissions": { + "name": "core_moderators_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "core_moderators_permissions_role_id_idx": { + "name": "core_moderators_permissions_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_moderators_permissions_user_id_idx": { + "name": "core_moderators_permissions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_moderators_permissions_roleId_core_roles_id_fk": { + "name": "core_moderators_permissions_roleId_core_roles_id_fk", + "tableFrom": "core_moderators_permissions", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_moderators_permissions_userId_core_users_id_fk": { + "name": "core_moderators_permissions_userId_core_users_id_fk", + "tableFrom": "core_moderators_permissions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_roles": { + "name": "core_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "root": { + "name": "root", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "guest": { + "name": "guest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "color": { + "name": "color", + "type": "varchar(19)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_sessions": { + "name": "core_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deviceId": { + "name": "deviceId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_sessions_user_id_idx": { + "name": "core_sessions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_sessions_userId_core_users_id_fk": { + "name": "core_sessions_userId_core_users_id_fk", + "tableFrom": "core_sessions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_sessions_deviceId_core_sessions_known_devices_id_fk": { + "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", + "tableFrom": "core_sessions", + "tableTo": "core_sessions_known_devices", + "columnsFrom": [ + "deviceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_sessions_token_unique": { + "name": "core_sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_sessions_known_devices": { + "name": "core_sessions_known_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastSeen": { + "name": "lastSeen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "core_sessions_known_devices_ip_address_idx": { + "name": "core_sessions_known_devices_ip_address_idx", + "columns": [ + { + "expression": "ipAddress", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_test": { + "name": "core_test", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users": { + "name": "core_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "nameCode": { + "name": "nameCode", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "newsletter": { + "name": "newsletter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatarColor": { + "name": "avatarColor", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "birthday": { + "name": "birthday", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'en'" + } + }, + "indexes": { + "core_users_name_code_idx": { + "name": "core_users_name_code_idx", + "columns": [ + { + "expression": "nameCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_name_idx": { + "name": "core_users_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_email_idx": { + "name": "core_users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_roleId_core_roles_id_fk": { + "name": "core_users_roleId_core_roles_id_fk", + "tableFrom": "core_users", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "core_users_language_core_languages_code_fk": { + "name": "core_users_language_core_languages_code_fk", + "tableFrom": "core_users", + "tableTo": "core_languages", + "columnsFrom": [ + "language" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set default", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_nameCode_unique": { + "name": "core_users_nameCode_unique", + "nullsNotDistinct": false, + "columns": [ + "nameCode" + ] + }, + "core_users_name_unique": { + "name": "core_users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + }, + "core_users_email_unique": { + "name": "core_users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_confirm_emails": { + "name": "core_users_confirm_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "core_users_confirm_emails_userId_core_users_id_fk": { + "name": "core_users_confirm_emails_userId_core_users_id_fk", + "tableFrom": "core_users_confirm_emails", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_confirm_emails_token_unique": { + "name": "core_users_confirm_emails_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_forgot_password": { + "name": "core_users_forgot_password", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "core_users_forgot_password_userId_core_users_id_fk": { + "name": "core_users_forgot_password_userId_core_users_id_fk", + "tableFrom": "core_users_forgot_password", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_forgot_password_userId_unique": { + "name": "core_users_forgot_password_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "userId" + ] + }, + "core_users_forgot_password_token_unique": { + "name": "core_users_forgot_password_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_sso": { + "name": "core_users_sso", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_users_sso_user_id_idx": { + "name": "core_users_sso_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_sso_userId_core_users_id_fk": { + "name": "core_users_sso_userId_core_users_id_fk", + "tableFrom": "core_users_sso", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.blog_categories": { + "name": "blog_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json index eab4a41d6..d68cbfca1 100644 --- a/apps/web/migrations/meta/_journal.json +++ b/apps/web/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1748527899355, "tag": "0000_wide_absorbing_man", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1748712016924, + "tag": "0001_material_elektra", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 914394f47..a67668721 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,7 +8,7 @@ "drizzle-kit": "drizzle-kit", "db:push": "drizzle-kit push", "db:migrate": "drizzle-kit up && drizzle-kit generate && drizzle-kit migrate", - "dev": "vitnode prepare && vitnode init && next dev --turbopack", + "dev": "vitnode init && next dev --turbopack", "dev:email": "email dev", "build": "next build --turbopack", "start": "next start", diff --git a/apps/web/src/app/[locale]/admin/(auth)/layout.tsx b/apps/web/src/app/[locale]/admin/(auth)/layout.tsx index 70967ed39..01f8ac1c5 100644 --- a/apps/web/src/app/[locale]/admin/(auth)/layout.tsx +++ b/apps/web/src/app/[locale]/admin/(auth)/layout.tsx @@ -1,7 +1,10 @@ -import { AdminLayout } from '@vitnode/core/views/admin/layouts/admin-layout'; +import { + AdminLayout, + type AdminLayoutProps, +} from '@vitnode/core/views/admin/layouts/admin-layout'; -export default function Layout( - props: React.ComponentProps, -) { - return ; +import { vitNodeConfig } from '@/vitnode.config'; + +export default function Layout(props: AdminLayoutProps) { + return ; } diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css deleted file mode 100644 index 3123b1d23..000000000 --- a/apps/web/src/app/globals.css +++ /dev/null @@ -1,119 +0,0 @@ -@import 'tailwindcss' source('../../src'); - -@import 'tw-animate-css'; - -@source "../../node_modules/@vitnode/core/dist/src/components"; -@source "../../node_modules/@vitnode/core/dist/src/views"; - -@custom-variant dark (&:is(.dark *)); - -:root:not(.dark) { - --background: oklch(1 0 0); - --foreground: oklch(0.141 0.005 285.823); - --card: oklch(1 0 0); - --card-foreground: oklch(0.141 0.005 285.823); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.141 0.005 285.823); - --primary: oklch(0.51 0.16 262.61); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.967 0.001 286.375); - --secondary-foreground: oklch(0.21 0.006 285.885); - --muted: oklch(0.967 0.001 286.375); - --muted-foreground: oklch(0.552 0.016 285.938); - --accent: oklch(0.94 0.005 286); /* Adjusted for better contrast on muted */ - --accent-foreground: oklch(0.21 0.006 285.885); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch( - 0.9 0.02 262.61 - ); /* Similar hue to dark primary, lighter/desaturated */ - --ring: oklch(0.7 0.16 262.61); /* Lighter version of dark primary */ - --chart-1: oklch(0.65 0.22 40); - --chart-2: oklch(0.6 0.15 185); - --chart-3: oklch(0.42 0.09 230); - --chart-4: oklch(0.83 0.19 85); - --chart-5: oklch(0.77 0.18 70); -} - -.dark { - --background: oklch(0.12 0 0); - --foreground: oklch(0.98 0.005 240); - --card: oklch(0.18 0.005 240); - --card-foreground: oklch(0.98 0.005 240); - --popover: oklch(0.18 0.005 240); - --popover-foreground: oklch(0.98 0.005 240); - --primary: oklch(0.51 0.16 262.61); - --primary-foreground: oklch(0.99 0.005 240); - --secondary: oklch(0.24 0.01 240); - --secondary-foreground: oklch(0.98 0.005 240); - --muted: oklch(0.22 0.01 240); /* Made slightly darker */ - --muted-foreground: oklch(0.75 0.005 240); - --accent: oklch(0.28 0.015 240); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(0.28 0.005 240); - --ring: oklch(0.51 0.16 262.61); - --chart-1: oklch(0.55 0.24 265); - --chart-2: oklch(0.7 0.17 160); - --chart-3: oklch(0.8 0.18 70); - --chart-4: oklch(0.7 0.26 300); - --chart-5: oklch(0.7 0.24 20); -} - -:root { - --radius: 0.625rem; - --input: var(--border); - --sidebar: var(--muted); - --sidebar-foreground: var(--foreground); - --sidebar-primary: var(--primary); - --sidebar-primary-foreground: var(--primary-foreground); - --sidebar-accent: var(--accent); - --sidebar-accent-foreground: var(--accent-foreground); - --sidebar-border: var(--border); - --sidebar-ring: var(--ring); -} - -@theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/apps/web/src/langs/@vitnode/blog/en.json b/apps/web/src/langs/@vitnode/blog/en.json new file mode 100644 index 000000000..13715c46d --- /dev/null +++ b/apps/web/src/langs/@vitnode/blog/en.json @@ -0,0 +1,5 @@ +{ + "blog": { + "title": "Blog from lang" + } +} diff --git a/apps/web/src/plugins/core/langs/en.json b/apps/web/src/langs/@vitnode/core/en.json similarity index 100% rename from apps/web/src/plugins/core/langs/en.json rename to apps/web/src/langs/@vitnode/core/en.json diff --git a/packages/create-vitnode-app/package.json b/packages/create-vitnode-app/package.json index dcb18a6ea..dbaa89825 100644 --- a/packages/create-vitnode-app/package.json +++ b/packages/create-vitnode-app/package.json @@ -16,7 +16,7 @@ }, "scripts": { "build:scripts": "tsc && node dist/src/prepare/prepare.js", - "start": "node dist/src/index.js", + "start:scripts": "node dist/src/index.js", "lint": "eslint .", "lint:fix": "eslint . --fix" }, diff --git a/packages/vitnode/components.json b/packages/vitnode/components.json index dcdd74483..d20ce6a8f 100644 --- a/packages/vitnode/components.json +++ b/packages/vitnode/components.json @@ -5,7 +5,7 @@ "tsx": true, "tailwind": { "config": "", - "css": "src/views/globals.css", + "css": "src/views/global.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" diff --git a/packages/vitnode/global.d.ts b/packages/vitnode/global.d.ts index ed26cf417..246ce81c6 100644 --- a/packages/vitnode/global.d.ts +++ b/packages/vitnode/global.d.ts @@ -1,6 +1,6 @@ -import type core from '../../apps/web/src/plugins/core/langs/en.json'; +import type plugin from './src/langs/en.json'; -type Messages = typeof core; +type Messages = typeof plugin; declare module 'next-intl' { interface AppConfig { diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json index e0c1e00f7..c09c354cb 100644 --- a/packages/vitnode/package.json +++ b/packages/vitnode/package.json @@ -57,7 +57,8 @@ "types": "./dist/src/*.d.ts", "default": "./dist/src/*.js" }, - "./config/next.config": "./config/next.config.ts" + "./config/next.config": "./config/next.config.ts", + "./langs/en.json": "./src/langs/en.json" }, "scripts": { "build:scripts": "tsup", diff --git a/packages/vitnode/scripts/plugin.ts b/packages/vitnode/scripts/plugin.ts index 1497df257..03f6ec84f 100644 --- a/packages/vitnode/scripts/plugin.ts +++ b/packages/vitnode/scripts/plugin.ts @@ -6,8 +6,10 @@ import { basename, join, relative } from 'path'; import { buildInitialRouteMap, copyFile, + findLocaleRoot, findRepoRoot, getAllFiles, + isDirectoryEmpty, routeKey, type SourceConfig, } from './shared/file-utils'; @@ -15,7 +17,7 @@ import { export const processPlugin = ({ initMessage }: { initMessage: string }) => { const pluginDir = process.cwd(); const repoRoot = findRepoRoot(pluginDir); - const localeRoot = join(repoRoot, 'apps', 'web', 'src', 'app', '[locale]'); + const localeRoot = findLocaleRoot(repoRoot); const routeMap = buildInitialRouteMap(localeRoot); // Get the package name from package.json for imports @@ -57,6 +59,7 @@ export const processPlugin = ({ initMessage }: { initMessage: string }) => { '(auth)', join('(plugins)', `(${pluginPathName})`), ); + const langDest = join(repoRoot, 'apps', 'web', 'src', 'langs', pluginName); // tell the copier about both trees const sources: SourceConfig[] = [ @@ -65,11 +68,16 @@ export const processPlugin = ({ initMessage }: { initMessage: string }) => { destinationDir: adminDest, }, { sourceDir: join(pluginDir, 'src', 'app'), destinationDir: mainDest }, + { sourceDir: join(pluginDir, 'src', 'langs'), destinationDir: langDest }, ]; - // Create destination directories if they don't exist - for (const { destinationDir } of sources) { - if (!existsSync(destinationDir)) { + // Create destination directories if they don't exist and source directories are not empty + for (const { sourceDir, destinationDir } of sources) { + if ( + existsSync(sourceDir) && + !isDirectoryEmpty(sourceDir) && + !existsSync(destinationDir) + ) { mkdirSync(destinationDir, { recursive: true }); } } @@ -123,7 +131,7 @@ export const processPlugin = ({ initMessage }: { initMessage: string }) => { const sourceDirs = sources .map(s => s.sourceDir) - .filter(dir => existsSync(dir)); + .filter(dir => existsSync(dir) && !isDirectoryEmpty(dir)); const watcher = chokidar.watch(sourceDirs, { ignoreInitial: false, diff --git a/packages/vitnode/scripts/prepare-database.ts b/packages/vitnode/scripts/prepare-database.ts index d2c7c00dd..5418ef059 100644 --- a/packages/vitnode/scripts/prepare-database.ts +++ b/packages/vitnode/scripts/prepare-database.ts @@ -10,14 +10,13 @@ import { core_languages, core_languages_words } from '@/database/languages.js'; import { core_moderators_permissions } from '@/database/moderators.js'; import { core_roles } from '@/database/roles.js'; +import { preparePluginsFiles } from './prepare-plugins-files.js'; import { runInteractiveShellCommand } from './run-interactive-shell-command.js'; dotenv.config({ path: join(process.cwd(), '..', '..', '.env'), }); -console.log(join(process.cwd(), '..', '..', '.env')); - const dbClient = drizzle({ connection: process.env.POSTGRES_URL ?? 'postgresql://root:root@localhost:5432/vitnode', @@ -154,11 +153,13 @@ export const prepareDatabase = async ({ }: { initMessage: string; }) => { - console.log(`${initMessage} [1/3] Generate migrations...`); + console.log(`${initMessage} [1/4] Prepare plugins files...`); + await preparePluginsFiles(); + console.log(`${initMessage} [2/4] Generate migrations...`); await generateDatabaseMigrations(); - console.log(`${initMessage} [2/3] Run migrations...`); + console.log(`${initMessage} [3/4] Run migrations...`); await runMigrations(); - console.log(`\n${initMessage} [3/3] Insert initial data...`); + console.log(`\n${initMessage} [4/4] Insert initial data...`); await initialDataForDatabase(); console.log(`${initMessage} \x1b[32mDatabase prepared successfully.\x1b[0m`); process.exit(0); diff --git a/packages/vitnode/scripts/prepare/prepare-plugins.ts b/packages/vitnode/scripts/prepare-plugins-files.ts similarity index 81% rename from packages/vitnode/scripts/prepare/prepare-plugins.ts rename to packages/vitnode/scripts/prepare-plugins-files.ts index c19fde63c..0b8267afd 100644 --- a/packages/vitnode/scripts/prepare/prepare-plugins.ts +++ b/packages/vitnode/scripts/prepare-plugins-files.ts @@ -2,23 +2,25 @@ import { existsSync, readFileSync } from 'fs'; import { join, relative } from 'path'; -import { getConfig } from '../get-config'; +import { getConfig } from './get-config'; import { buildInitialRouteMap, copyDirectoryRecursive, + findLocaleRoot, findRepoRoot, + isDirectoryEmpty, type SourceConfig, -} from '../shared/file-utils'; +} from './shared/file-utils'; -export const preparePlugins = async () => { +export const preparePluginsFiles = async () => { const config = await getConfig(); const plugins: string[] = [ - ...config.plugins.map(plugin => plugin.name), + ...config.plugins.map(plugin => plugin.id), 'vitnode', ]; const repoRoot = findRepoRoot(process.cwd()); - const localeRoot = join(repoRoot, 'apps', 'web', 'src', 'app', '[locale]'); + const localeRoot = findLocaleRoot(repoRoot); const routeMap = buildInitialRouteMap(localeRoot); await Promise.all( @@ -68,6 +70,14 @@ export const preparePlugins = async () => { '(auth)', join('(plugins)', `(${pluginPathName})`), ); + const langDest = join( + repoRoot, + 'apps', + 'web', + 'src', + 'langs', + pluginName, + ); // Define source configurations for this plugin const sources: SourceConfig[] = [ @@ -79,11 +89,15 @@ export const preparePlugins = async () => { sourceDir: join(pluginPath, 'src', 'app'), destinationDir: mainDest, }, + { + sourceDir: join(pluginPath, 'src', 'langs'), + destinationDir: langDest, + }, ]; // Copy files for each source directory for (const { sourceDir, destinationDir } of sources) { - if (existsSync(sourceDir)) { + if (existsSync(sourceDir) && !isDirectoryEmpty(sourceDir)) { console.log( `\x1b[36mCopying ${pluginName}:\x1b[0m ${relative(repoRoot, sourceDir)} → ${relative(repoRoot, destinationDir)}`, ); diff --git a/packages/vitnode/scripts/prepare/prepare-files.ts b/packages/vitnode/scripts/prepare/prepare-files.ts deleted file mode 100644 index 3f7d88e66..000000000 --- a/packages/vitnode/scripts/prepare/prepare-files.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable no-console */ - -import { preparePlugins } from './prepare-plugins'; - -export const prepareFiles = async ({ - initMessage, -}: { - initMessage: string; -}) => { - console.log(`${initMessage} Preparing files...`); - await preparePlugins(); - console.log(`${initMessage} \x1b[32mFiles prepared successfully.\x1b[0m`); - process.exit(0); -}; diff --git a/packages/vitnode/scripts/scripts.ts b/packages/vitnode/scripts/scripts.ts index 8f0ff8b66..a64e60320 100644 --- a/packages/vitnode/scripts/scripts.ts +++ b/packages/vitnode/scripts/scripts.ts @@ -3,7 +3,6 @@ import { processPlugin } from './plugin.js'; import { prepareDatabase } from './prepare-database.js'; -import { prepareFiles } from './prepare/prepare-files.js'; const initMessage = '\x1b[34m[VitNode]\x1b[0m'; @@ -21,10 +20,6 @@ switch (command) { } break; - case 'prepare': - void prepareFiles({ initMessage }); - break; - default: console.log( `${initMessage} \x1b[31mCommand not found: "${command ?? ''}"\x1b[0m`, diff --git a/packages/vitnode/scripts/shared/file-utils.ts b/packages/vitnode/scripts/shared/file-utils.ts index 27c47ef04..12dbd7d7f 100644 --- a/packages/vitnode/scripts/shared/file-utils.ts +++ b/packages/vitnode/scripts/shared/file-utils.ts @@ -124,6 +124,23 @@ export function findRepoRoot(start: string): string { ); } +export function findLocaleRoot(repoRoot: string): string { + // Check for standalone structure (src/app/[locale]) + const standalonePath = join(repoRoot, 'src', 'app', '[locale]'); + if (existsSync(standalonePath)) { + return standalonePath; + } + + // Check for monorepo structure first (apps/web/src/app/[locale]) + const monorepoPath = join(repoRoot, 'apps', 'web', 'src', 'app', '[locale]'); + if (existsSync(monorepoPath)) { + return monorepoPath; + } + + // Default to monorepo structure if neither exists (for new projects) + return monorepoPath; +} + export const getAllFiles = (dir: string): string[] => { const files: string[] = []; if (!existsSync(dir)) return files; @@ -142,6 +159,14 @@ export const getAllFiles = (dir: string): string[] => { return files; }; +export const isDirectoryEmpty = (dir: string): boolean => { + if (!existsSync(dir)) return true; + + const files = getAllFiles(dir); + + return files.length === 0; +}; + export interface SourceConfig { destinationDir: string; sourceDir: string; @@ -231,7 +256,7 @@ export const copyDirectoryRecursive = ( repoRoot: string, verbose = true, ) => { - if (!existsSync(sourceDir)) return; + if (!existsSync(sourceDir) || isDirectoryEmpty(sourceDir)) return; const files = getAllFiles(sourceDir); diff --git a/packages/vitnode/src/api/config.ts b/packages/vitnode/src/api/config.ts index 034d6281f..13c1fa417 100644 --- a/packages/vitnode/src/api/config.ts +++ b/packages/vitnode/src/api/config.ts @@ -92,7 +92,7 @@ export function VitNodeAPI({ }); [newBuildPluginApiCore, ...vitNodeApiConfig.plugins].map(root => { - app.route(`/${root.name}`, root.hono); + app.route(`/${root.id}`, root.hono); }); return app; diff --git a/packages/vitnode/src/api/lib/check-plugin-name.ts b/packages/vitnode/src/api/lib/check-plugin-id.ts similarity index 93% rename from packages/vitnode/src/api/lib/check-plugin-name.ts rename to packages/vitnode/src/api/lib/check-plugin-id.ts index 32c7dfa2f..e57cb477b 100644 --- a/packages/vitnode/src/api/lib/check-plugin-name.ts +++ b/packages/vitnode/src/api/lib/check-plugin-id.ts @@ -17,7 +17,7 @@ export interface PackageJSON { workspaces?: string[]; } -export const checkPluginName = (pluginName: string): null | PackageJSON => { +export const checkPluginId = (pluginName: string): null | PackageJSON => { if (!CONFIG.node_development) return null; const path = join(process.cwd(), 'node_modules', pluginName, 'package.json'); diff --git a/packages/vitnode/src/api/lib/plugin.ts b/packages/vitnode/src/api/lib/plugin.ts index 4774ff86d..39ab40933 100644 --- a/packages/vitnode/src/api/lib/plugin.ts +++ b/packages/vitnode/src/api/lib/plugin.ts @@ -2,22 +2,22 @@ import { OpenAPIHono } from '@hono/zod-openapi'; import type { BuildModuleReturn } from './module'; -import { checkPluginName } from './check-plugin-name'; +import { checkPluginId } from './check-plugin-id'; export interface BuildPluginApiReturn { hono: OpenAPIHono; - name: string; + id: string; } export function buildApiPlugin

({ - name, + id, modules = [], }: { + id: P; modules?: BuildModuleReturn[]; - name: P; }): BuildPluginApiReturn { // Run for checking if the plugin is valid - checkPluginName(name); + checkPluginId(id); const hono = new OpenAPIHono(); modules.forEach(handler => { @@ -25,7 +25,7 @@ export function buildApiPlugin

({ }); return { - name: name, + id, hono, }; } diff --git a/packages/vitnode/src/api/plugin.ts b/packages/vitnode/src/api/plugin.ts index 8b9570d88..232243977 100644 --- a/packages/vitnode/src/api/plugin.ts +++ b/packages/vitnode/src/api/plugin.ts @@ -4,6 +4,6 @@ import { middlewareModule } from './modules/middleware/middleware.module'; import { usersModule } from './modules/users/users.module'; export const newBuildPluginApiCore = buildApiPlugin({ - name: '@vitnode/core', + id: '@vitnode/core', modules: [middlewareModule, usersModule, adminModule], }); diff --git a/packages/vitnode/src/components/i18n-provider.tsx b/packages/vitnode/src/components/i18n-provider.tsx index c1c8400fe..a49aea6a8 100644 --- a/packages/vitnode/src/components/i18n-provider.tsx +++ b/packages/vitnode/src/components/i18n-provider.tsx @@ -39,7 +39,7 @@ export async function I18nProvider< namespaces: NestedKey | NestedKey[]; }) { const locale = await getLocale(); - const messagesInit = await getMessages({ locale }); + const messagesInit: object = await getMessages({ locale }); const messages = pick(messagesInit, [ 'core.global', ...(Array.isArray(namespaces) ? namespaces : [namespaces]), diff --git a/packages/vitnode/src/drizzle.config.ts b/packages/vitnode/src/drizzle.config.ts index e4c54b415..eaa90b017 100644 --- a/packages/vitnode/src/drizzle.config.ts +++ b/packages/vitnode/src/drizzle.config.ts @@ -12,14 +12,14 @@ export const defineVitNodeDrizzleConfig = ({ }: Config & { vitNodeApiConfig: VitNodeApiConfig; }) => { - const pluginNames = vitNodeApiConfig.plugins.map(plugin => plugin.name); + const pluginId = vitNodeApiConfig.plugins.map(plugin => plugin.id); - const pluginPaths = ['@vitnode/core', ...pluginNames] - .map(pluginName => { + const pluginPaths = ['@vitnode/core', ...pluginId] + .map(itemId => { const pluginPath = resolve( process.cwd(), 'node_modules', - pluginName, + itemId, 'src', 'database', ); diff --git a/apps/web/src/plugins/core/langs/pl.json b/packages/vitnode/src/langs/en.json similarity index 73% rename from apps/web/src/plugins/core/langs/pl.json rename to packages/vitnode/src/langs/en.json index 2bd862077..60ce93ca3 100644 --- a/apps/web/src/plugins/core/langs/pl.json +++ b/packages/vitnode/src/langs/en.json @@ -9,6 +9,9 @@ }, "core": { "global": { + "date": "{date, date}", + "date_medium": "{date, date, medium}", + "date_short": "{date, date, short}", "register": "Register", "login": "Login", "save": "Save", @@ -19,6 +22,10 @@ "or": "or", "back_home": "Back to home", "go_back": "Go back", + "select_option": "Select an option", + "select_options": "Select options", + "go_to_prev_page": "Go to previous page", + "go_to_next_page": "Go to next page", "errors": { "title": "Oops! Something went wrong.", "internal_server_error": "Internal server error.", @@ -41,7 +48,8 @@ } }, "user_bar": { - "log_out": "Log out" + "log_out": "Log out", + "admin_cp": "Admin CP" } }, "auth": { @@ -50,9 +58,9 @@ "access_denied": "You have denied access to the application or the request has expired. Please try again." }, "sign_up": { - "desc": "Create your account to get started.", + "desc": "Hello there! Create your account to get started.", "already_have_account": "You already have an account? Sign in.", - "submit": "Sign up", + "submit": "Register", "username": { "label": "Username", "min_length": "Username must be at least 3 characters long.", @@ -87,20 +95,49 @@ } }, "sign_in": { - "desc": "Welcome back! Please sign in to your account.", + "desc": "Welcome back! Sign in to your account.", "do_not_have_account": "Don't have an account? Sign up.", "email": { "label": "Email", "invalid": "Invalid email address." }, - "password": "Password", + "password": { + "label": "Password", + "required": "Password is required." + }, "errors": { "access_denied": { "title": "Invalid credentials", "desc": "The email address or password was incorrect. Please try again (make sure your caps lock is off)." } }, - "submit": "Sign in" + "submit": "Login" + } + } + }, + "admin": { + "dashboard": { + "dev_mode": "Development Mode", + "version": "Version: {version}" + }, + "global": { + "nav": { + "core": "Core", + "dashboard": "Dashboard", + "users": { + "title": "Users", + "list": "User List" + }, + "user_bar": { + "home_page": "Home Page" + } + } + }, + "user": { + "list": { + "user": "User", + "createdAt": "Created At", + "emailNotVerified": "Email Not Verified" } } } diff --git a/packages/vitnode/src/lib/plugin.ts b/packages/vitnode/src/lib/plugin.ts index 7f0d9435e..9620a344a 100644 --- a/packages/vitnode/src/lib/plugin.ts +++ b/packages/vitnode/src/lib/plugin.ts @@ -1,5 +1,8 @@ export interface BuildPluginReturn

{ - name: P; + adminNav?: { + label: string; + }[]; + id: P; } export function buildPlugin

( diff --git a/packages/vitnode/src/views/admin/layouts/admin-layout.tsx b/packages/vitnode/src/views/admin/layouts/admin-layout.tsx index 1fcba12b8..e87a8a9b4 100644 --- a/packages/vitnode/src/views/admin/layouts/admin-layout.tsx +++ b/packages/vitnode/src/views/admin/layouts/admin-layout.tsx @@ -1,3 +1,4 @@ +import { ListIcon } from 'lucide-react'; import { cookies } from 'next/headers'; import { ThemeSwitcher } from '@/components/switchers/theme-switcher'; @@ -5,23 +6,50 @@ import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; import { SidebarInset } from '@/components/ui/sidebar'; import { getSessionAdminApi } from '@/lib/api/get-session-admin-api'; +import type { VitNodeConfig } from '../../../vitnode.config'; +import type { NavAdminParent } from './sidebar/nav/nav'; + import { SidebarAdmin } from './sidebar/sidebar'; import { UserBarAdmin } from './user-bar/user-bar'; +export interface AdminLayoutProps { + children: React.ReactNode; +} + export const AdminLayout = async ({ children, -}: { - children: React.ReactNode; - params: Promise<{ locale: string }>; + vitNodeConfig, +}: AdminLayoutProps & { + vitNodeConfig: VitNodeConfig; }) => { const session = await getSessionAdminApi(); const cookieStore = await cookies(); const defaultOpen = cookieStore.get('sidebar_state')?.value === 'true'; if (!session) return null; + const pluginNav: NavAdminParent[] = vitNodeConfig.plugins + .filter(plugin => plugin.adminNav) + .map(plugin => ({ + id: plugin.id, + title: plugin.id, + items: [ + { + href: '/admin/blog/categories', + title: 'Categories', + icon: , + items: [ + { + href: '/admin/blog/categories', + title: 'List', + }, + ], + }, + ], + })); + return ( - +

diff --git a/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav.tsx b/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav.tsx index 8dce739b8..7b587aa7a 100644 --- a/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav.tsx +++ b/packages/vitnode/src/views/admin/layouts/sidebar/nav/nav.tsx @@ -15,7 +15,11 @@ export interface NavAdminParent { title: string; } -export const NavSidebarAdmin = async () => { +export const NavSidebarAdmin = async ({ + pluginNav, +}: { + pluginNav: NavAdminParent[]; +}) => { const t = await getTranslations('admin.global.nav'); const rootItems: NavAdminParent[] = [ { @@ -44,6 +48,7 @@ export const NavSidebarAdmin = async () => { }, ], }, + ...pluginNav, ]; return rootItems.map(parent => ( diff --git a/packages/vitnode/src/views/admin/layouts/sidebar/sidebar.tsx b/packages/vitnode/src/views/admin/layouts/sidebar/sidebar.tsx index 55a2f0708..8fc6f5459 100644 --- a/packages/vitnode/src/views/admin/layouts/sidebar/sidebar.tsx +++ b/packages/vitnode/src/views/admin/layouts/sidebar/sidebar.tsx @@ -5,7 +5,9 @@ import { Link } from '@/lib/navigation'; import { NavSidebarAdmin } from './nav/nav'; -export const SidebarAdmin = () => { +export const SidebarAdmin = ({ + pluginNav, +}: React.ComponentProps) => { return ( @@ -14,7 +16,7 @@ export const SidebarAdmin = () => { - + ); diff --git a/packages/vitnode/src/vitnode.config.ts b/packages/vitnode/src/vitnode.config.ts index d4ff34c3c..06af3f539 100644 --- a/packages/vitnode/src/vitnode.config.ts +++ b/packages/vitnode/src/vitnode.config.ts @@ -70,9 +70,28 @@ export const handleRequestConfig = async ({ ? reqLocale : vitNodeConfig.i18n.defaultLocale; + const pluginsId: string[] = [ + '@vitnode/core', + ...vitNodeConfig.plugins.map(plugin => plugin.id), + ]; + + // Import and merge messages from all plugins + const messagesPromises = pluginsId.map(async pluginId => { + try { + const messages = await import(`@/langs/${pluginId}/${locale}.json`); + + return messages.default; + } catch { + return {}; + } + }); + + const allMessages = await Promise.all(messagesPromises); + const messages = allMessages.reduce((acc, curr) => ({ ...acc, ...curr }), {}); + return { locale, - messages: (await import(`@/plugins/core/langs/${locale}.json`)).default, + messages, timeZone: vitNodeConfig.i18n.timeZone, }; }; diff --git a/packages/vitnode/tsconfig.json b/packages/vitnode/tsconfig.json index 81b05a682..5fee1e2aa 100644 --- a/packages/vitnode/tsconfig.json +++ b/packages/vitnode/tsconfig.json @@ -24,9 +24,9 @@ "include": [ "src", "scripts", - "global.d.ts", "emails", "vitest.config.ts", - "tsup.config.ts" + "tsup.config.ts", + "global.d.ts" ] } diff --git a/plugins/blog/global.d.ts b/plugins/blog/global.d.ts new file mode 100644 index 000000000..7fcfebb9b --- /dev/null +++ b/plugins/blog/global.d.ts @@ -0,0 +1,10 @@ +import type plugin from './src/langs/en.json'; +import type core from '@vitnode/core/langs/en.json'; + +type Messages = typeof plugin & typeof core; + +declare module 'next-intl' { + interface AppConfig { + Messages: Messages; + } +} diff --git a/plugins/blog/src/config.ts b/plugins/blog/src/config.ts index 38223aa5d..e70da1587 100644 --- a/plugins/blog/src/config.ts +++ b/plugins/blog/src/config.ts @@ -1,3 +1,3 @@ export const configPlugin = { - name: '@vitnode/blog' as const, + id: '@vitnode/blog' as const, }; diff --git a/plugins/blog/src/database/categories.ts b/plugins/blog/src/database/categories.ts new file mode 100644 index 000000000..4c35e81d9 --- /dev/null +++ b/plugins/blog/src/database/categories.ts @@ -0,0 +1,10 @@ +import { pgTable } from 'drizzle-orm/pg-core'; + +export const blog_categories = pgTable('blog_categories', t => ({ + id: t.serial().primaryKey(), + createdAt: t.timestamp().notNull().defaultNow(), + updatedAt: t + .timestamp() + .notNull() + .$onUpdate(() => new Date()), +})).enableRLS(); diff --git a/plugins/blog/src/langs/en.json b/plugins/blog/src/langs/en.json new file mode 100644 index 000000000..13715c46d --- /dev/null +++ b/plugins/blog/src/langs/en.json @@ -0,0 +1,5 @@ +{ + "blog": { + "title": "Blog from lang" + } +} diff --git a/plugins/blog/src/plugin.tsx b/plugins/blog/src/plugin.tsx index 6f36f84bb..8b7c8f4b7 100644 --- a/plugins/blog/src/plugin.tsx +++ b/plugins/blog/src/plugin.tsx @@ -5,5 +5,10 @@ import { configPlugin } from './config'; export const blogPlugin = () => { return buildPlugin({ ...configPlugin, + adminNav: [ + { + label: 'Blog', + }, + ], }); }; diff --git a/plugins/blog/src/views/test.tsx b/plugins/blog/src/views/test.tsx index 18c67c7f2..a328db40c 100644 --- a/plugins/blog/src/views/test.tsx +++ b/plugins/blog/src/views/test.tsx @@ -1,7 +1,11 @@ +import { useTranslations } from 'next-intl'; + export const Test = () => { + const t = useTranslations('blog'); + return (
-

Test

+

{t('title')}

This is a test page.

); diff --git a/plugins/blog/tsconfig.json b/plugins/blog/tsconfig.json index b6ebccf41..85835397c 100644 --- a/plugins/blog/tsconfig.json +++ b/plugins/blog/tsconfig.json @@ -21,5 +21,5 @@ } }, "exclude": ["node_modules"], - "include": ["src"] + "include": ["src", "global.d.ts"] } diff --git a/scripts/files/file-copy-manager.ts b/scripts/files/file-copy-manager.ts index cd16dd327..bbbf5ce80 100644 --- a/scripts/files/file-copy-manager.ts +++ b/scripts/files/file-copy-manager.ts @@ -53,7 +53,7 @@ export class FileCopyManager { 'src/app/[locale]', 'src/app/favicon.ico', 'src/app/global-error.tsx', - 'src/app/globals.css', + 'src/app/global.css', 'src/app/layout.tsx', 'src/app/not-found.tsx', 'src/app/api', diff --git a/turbo.json b/turbo.json index 6b1506736..420b73684 100644 --- a/turbo.json +++ b/turbo.json @@ -45,8 +45,7 @@ "dependsOn": ["^lint"] }, "lint:fix": { - "dependsOn": ["^lint:fix"], - "cache": false + "dependsOn": ["^lint:fix"] }, "dev": { "cache": false,