,
-) {
- 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,