diff --git a/apps/docs/content/docs/plugins/admin-page.mdx b/apps/docs/content/docs/dev/admin-page.mdx similarity index 100% rename from apps/docs/content/docs/plugins/admin-page.mdx rename to apps/docs/content/docs/dev/admin-page.mdx diff --git a/apps/docs/content/docs/dev/auth.mdx b/apps/docs/content/docs/dev/auth.mdx index 9ac1d7af7..686c371cb 100644 --- a/apps/docs/content/docs/dev/auth.mdx +++ b/apps/docs/content/docs/dev/auth.mdx @@ -3,6 +3,10 @@ title: Authorization description: xddd --- + + We're working hard to bring you the best documentation experience. + + ## Configuration You can configure the authentication settings in the `VitNodeAPI` function and `authorization` param. diff --git a/apps/docs/content/docs/dev/database/index.mdx b/apps/docs/content/docs/dev/database/index.mdx new file mode 100644 index 000000000..167663301 --- /dev/null +++ b/apps/docs/content/docs/dev/database/index.mdx @@ -0,0 +1,96 @@ +--- +title: Database +description: Learn how to work with databases in VitNode plugins using Drizzle ORM and PostgreSQL. +--- + +VitNode plugins seamlessly integrate with databases using [Drizzle ORM](https://orm.drizzle.team/) and [PostgreSQL](https://www.postgresql.org/). This guide shows you how to define schemas, perform database operations, and manage migrations. + +## Defining Schema + +Create your database schema in the `database` directory of your plugin. Each table should be defined in its own file for better organization. + +```ts title="plugins/{plugin_name}/src/database/categories.ts" +import { pgTable, serial, timestamp } from 'drizzle-orm/pg-core'; + +export const blog_categories = pgTable('blog_categories', { + id: serial().primaryKey(), + createdAt: timestamp().notNull().defaultNow(), + updatedAt: timestamp() + .notNull() + .$onUpdate(() => new Date()), +}); +``` + +## Accessing Database + +Access the database in your plugin handlers using `c.get('database')` from the Hono context. This provides a Drizzle ORM instance for all your database operations. + +```ts title="plugins/{plugin_name}/src/routes/posts.ts" +export const postsRoute = buildRoute({ + ...CONFIG_PLUGIN, + route: {}, + handler: async c => { + // [!code ++:7] + const data = await c + .get('database') + .select({ + id: blog_posts.id, + title: blog_posts.title, + }) + .from(blog_posts); + + return c.json(data); + }, +}); +``` + +## Database Operations + +VitNode provides convenient commands for managing your database schema and migrations. + +### Creating Migrations + +Generate migration files when you modify your database schema: + +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + + + +```bash tab="pnpm" +pnpm db:migrate +``` + +```bash tab="bun" +bun db:migrate +``` + +```bash tab="npm" +npm run db:migrate +``` + + + +### Pushing Schema Changes + +Apply your schema changes directly to the database: + + + +```bash tab="pnpm" +pnpm db:push +``` + +```bash tab="bun" +bun db:push +``` + +```bash tab="npm" +npm run db:push +``` + + + + + VitNode automatically runs migrations and schema updates when you start your + application in development mode, keeping your database in sync with your code. + diff --git a/apps/docs/content/docs/dev/pagination.mdx b/apps/docs/content/docs/dev/database/pagination.mdx similarity index 100% rename from apps/docs/content/docs/dev/pagination.mdx rename to apps/docs/content/docs/dev/database/pagination.mdx diff --git a/apps/docs/content/docs/dev/i18n/index.mdx b/apps/docs/content/docs/dev/i18n/index.mdx new file mode 100644 index 000000000..9369ef8fc --- /dev/null +++ b/apps/docs/content/docs/dev/i18n/index.mdx @@ -0,0 +1,10 @@ +--- +title: Expand Languages +description: Expand the languages to your application. +--- + + + We're working hard to bring you the best documentation experience. + + +As default VitNode uses the `en` _(English USA)_ language for the application. diff --git a/apps/docs/content/docs/dev/i18n/meta.json b/apps/docs/content/docs/dev/i18n/meta.json new file mode 100644 index 000000000..1efaf1161 --- /dev/null +++ b/apps/docs/content/docs/dev/i18n/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Internationalization (I18n)", + "pages": ["expand-langs", "messages", "namespaces", "..."] +} diff --git a/apps/docs/content/docs/dev/i18n/namespaces.mdx b/apps/docs/content/docs/dev/i18n/namespaces.mdx index 3d65491c1..55e5efa41 100644 --- a/apps/docs/content/docs/dev/i18n/namespaces.mdx +++ b/apps/docs/content/docs/dev/i18n/namespaces.mdx @@ -16,43 +16,24 @@ For example, you have a component that needs to get access to the translation st To translate your content, select the plugin in `frontend` folder, go to `langs` folder and pick the language you want to translate. -```json title="apps/frontend/src/plugins/{your_plugin}/langs/en.json" -{ - "{your_plugin}": { - "home": { - "hello": "Hello World", - "world": "World" - } - } -} -``` - - - Name your plugin should be a primary key in the JSON file or with the prefix `admin_` for admin plugins. - -```json title="apps/frontend/src/plugins/{your_plugin}/langs/en.json" +```json title="plugins/{your_plugin}/src/locales/en.json" { "{your_plugin}": { "hello": "Hello World" // ✅ This will work }, - "admin_{your_plugin}" { - "world": "World" // ✅ This will work - }, "world": "World" // ❌ This will not work } ``` - - ## Usage To get access to the translation strings in other namespaces you need to use the `TranslationsProvider` component. -```tsx title="apps/frontend/src/app/[locale]/(main)/{your_plugin}/layout.tsx" +```tsx title="plugins/{your_plugin}/src/app/blog/layout.tsx" import { I18nProvider } from '@vitnode/core/components/i18n-provider'; export default function Layout({ children }: { children: React.ReactNode }) { - return {children}; + return {children}; } ``` diff --git a/apps/docs/content/docs/dev/index.mdx b/apps/docs/content/docs/dev/index.mdx index 487673076..fd066bb90 100644 --- a/apps/docs/content/docs/dev/index.mdx +++ b/apps/docs/content/docs/dev/index.mdx @@ -4,6 +4,10 @@ description: Welcome to the VitNode documentation! icon: Power --- + + We're working hard to bring you the best documentation experience. + + ## Get started import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; diff --git a/apps/docs/content/docs/plugins/layouts-and-pages.mdx b/apps/docs/content/docs/dev/layouts-and-pages.mdx similarity index 100% rename from apps/docs/content/docs/plugins/layouts-and-pages.mdx rename to apps/docs/content/docs/dev/layouts-and-pages.mdx diff --git a/apps/docs/content/docs/dev/meta.json b/apps/docs/content/docs/dev/meta.json index c78606f74..990412e35 100644 --- a/apps/docs/content/docs/dev/meta.json +++ b/apps/docs/content/docs/dev/meta.json @@ -11,9 +11,16 @@ "swagger", "---Framework---", "auth", - "---API---", + "---Plugins---", + "plugins", + "rest-api", "fetcher", - "---Rest---", + "database", + "---UI---", + "layouts-and-pages", + "admin-page", + "i18n", + "---Advanced---", "..." ] } diff --git a/apps/docs/content/docs/dev/plugins.mdx b/apps/docs/content/docs/dev/plugins.mdx new file mode 100644 index 000000000..8fa5c705d --- /dev/null +++ b/apps/docs/content/docs/dev/plugins.mdx @@ -0,0 +1,49 @@ +--- +title: Create Plugins +description: Learn how to create plugins for VitNode to extend its functionality. +--- + + + We're working hard to bring you the best documentation experience. + + +In this section we'll guide you through the process of creating your first VitNode plugin. Whether you're building a simple feature or a complex application, VitNode's plugin system has got you covered. + +By creating a plugin, you can extend the functionality like: + +- **🚀 APIs**: Build your own RESTful, +- **🧩 UI Components**: Create reusable components for your application, +- **📄 Pages and Layouts**: Define custom pages and layouts for your application, +- **⚡ Middleware**: Add custom middleware to handle requests and responses, +- **🗄️ Database Models**: Define your own database models and schemas, +- **🔗 Integrations**: Connect with third-party services and APIs. +- **⚙️ Custom Logic**: Implement any custom logic you need for your application. +- **🌟 And much more!** + +## Create a Plugin + +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + + + +```bash tab="pnpm" +pnpm create vitnode-app@canary --plugin +``` + +```bash tab="bun" +bun create vitnode-app@canary --plugin +``` + +```bash tab="npm" +npx create-vitnode-app@canary --plugin +``` + + + +## Prerequisites + +### Ignore `(plugins)` Folders + +Before diving into plugin development, please ignore all `(plugins)` folders form search results in your IDE. These folders are generated by VitNode and contain the plugin code, which is not meant to be modified directly. + +If you're using VS Code, this configuration is provided out-of-the-box via the `.vscode/settings.json` file. diff --git a/apps/docs/content/docs/plugins/routing.mdx b/apps/docs/content/docs/dev/rest-api.mdx similarity index 99% rename from apps/docs/content/docs/plugins/routing.mdx rename to apps/docs/content/docs/dev/rest-api.mdx index e0eaea045..cdf7e904c 100644 --- a/apps/docs/content/docs/plugins/routing.mdx +++ b/apps/docs/content/docs/dev/rest-api.mdx @@ -1,5 +1,5 @@ --- -title: Routing +title: Restful API description: Learn how to create and organize API routes in your VitNode plugins with modules, handlers, and parameter validation. --- diff --git a/apps/docs/content/docs/plugins/sso.mdx b/apps/docs/content/docs/dev/sso.mdx similarity index 99% rename from apps/docs/content/docs/plugins/sso.mdx rename to apps/docs/content/docs/dev/sso.mdx index fb9bfafc8..aed47d6d4 100644 --- a/apps/docs/content/docs/plugins/sso.mdx +++ b/apps/docs/content/docs/dev/sso.mdx @@ -333,7 +333,7 @@ VitNodeAPI({ plugins: [], authorization: { // [!code ++] - ssoPlugins: [ + ssoProviders: [ // [!code ++] DiscordSSOApiPlugin({ // [!code ++] diff --git a/apps/docs/content/docs/dev/swagger.mdx b/apps/docs/content/docs/dev/swagger.mdx index 8832d7257..375459a50 100644 --- a/apps/docs/content/docs/dev/swagger.mdx +++ b/apps/docs/content/docs/dev/swagger.mdx @@ -4,6 +4,10 @@ description: Generate API documentation with OpenAPI. icon: ListCollapse --- + + We're working hard to bring you the best documentation experience. + + Swagger is a tool that helps you generate API documentation with OpenAPI. It's a powerful tool that can help you write documentation automatically and validate the request. ## Usage diff --git a/apps/docs/content/docs/plugins/blog.mdx b/apps/docs/content/docs/guides/blog.mdx similarity index 100% rename from apps/docs/content/docs/plugins/blog.mdx rename to apps/docs/content/docs/guides/blog.mdx diff --git a/apps/docs/content/docs/guides/meta.json b/apps/docs/content/docs/guides/meta.json index 4688a8787..3e8466f9a 100644 --- a/apps/docs/content/docs/guides/meta.json +++ b/apps/docs/content/docs/guides/meta.json @@ -6,6 +6,8 @@ "pages": [ "index", "...", + "---Plugins by VitNode---", + "blog", "--- Authorization ---", "sso", "--- Security ---", diff --git a/apps/docs/content/docs/guides/sso/discord.mdx b/apps/docs/content/docs/guides/sso/discord.mdx index 69fa65f35..edc1e382d 100644 --- a/apps/docs/content/docs/guides/sso/discord.mdx +++ b/apps/docs/content/docs/guides/sso/discord.mdx @@ -68,7 +68,7 @@ VitNodeAPI({ // [!code ++] authorization: { // [!code ++] - ssoPlugins: [ + ssoProviders: [ // [!code ++] new DiscordSSOApiPlugin({ // [!code ++] diff --git a/apps/docs/content/docs/guides/sso/facebook.mdx b/apps/docs/content/docs/guides/sso/facebook.mdx index f1d1486f3..8674b2f01 100644 --- a/apps/docs/content/docs/guides/sso/facebook.mdx +++ b/apps/docs/content/docs/guides/sso/facebook.mdx @@ -62,7 +62,7 @@ VitNodeAPI({ // [!code ++] authorization // [!code ++] - ssoPlugins: [ + ssoProviders: [ // [!code ++] new FacebookSSOApiPlugin({ // [!code ++] diff --git a/apps/docs/content/docs/guides/sso/google.mdx b/apps/docs/content/docs/guides/sso/google.mdx index d9a9edcf2..c5ce45613 100644 --- a/apps/docs/content/docs/guides/sso/google.mdx +++ b/apps/docs/content/docs/guides/sso/google.mdx @@ -123,7 +123,7 @@ VitNodeAPI({ // [!code ++] authorization: { // [!code ++] - ssoPlugins: [ + ssoProviders: [ // [!code ++] new GoogleSSOApiPlugin({ // [!code ++] diff --git a/apps/docs/content/docs/plugins/database/index.mdx b/apps/docs/content/docs/plugins/database/index.mdx deleted file mode 100644 index 6c5d93bc9..000000000 --- a/apps/docs/content/docs/plugins/database/index.mdx +++ /dev/null @@ -1,23 +0,0 @@ ---- -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/index.mdx b/apps/docs/content/docs/plugins/index.mdx deleted file mode 100644 index 530652cd4..000000000 --- a/apps/docs/content/docs/plugins/index.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Get Started -description: Create awesome plugins for VitNode. -icon: House ---- - - - We're working on the plugin system, so stay tuned for updates! 🚀 - - -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; - - - -```bash tab="pnpm" -pnpm create vitnode-app@canary --plugin -``` - -```bash tab="bun" -bun create vitnode-app@canary --plugin -``` - -```bash tab="npm" -npx create-vitnode-app@canary --plugin -``` - - diff --git a/apps/docs/content/docs/plugins/meta.json b/apps/docs/content/docs/plugins/meta.json deleted file mode 100644 index f0b4d4416..000000000 --- a/apps/docs/content/docs/plugins/meta.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "title": "Plugins", - "description": "Create and share own plugins", - "icon": "Plug", - "root": true, - "pages": [ - "index", - "---Plugins by VitNode---", - "blog", - "---API---", - "routing", - "database", - "---UI---", - "layouts-and-pages", - "admin-page", - "---Advanced---", - "..." - ] -} diff --git a/apps/docs/content/docs/ui/dropdown-menu.mdx b/apps/docs/content/docs/ui/dropdown-menu.mdx new file mode 100644 index 000000000..807fbe96d --- /dev/null +++ b/apps/docs/content/docs/ui/dropdown-menu.mdx @@ -0,0 +1,31 @@ +--- +title: Dropdown Menu +description: A dropdown menu component for building interactive menus in your application. +--- + +## Usage + +```ts +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@vitnode/core/components/ui/dropdown-menu'; +``` + +```tsx + + Open + + My Account + + Profile + Billing + Team + Subscription + + +``` diff --git a/apps/docs/content/docs/ui/index.mdx b/apps/docs/content/docs/ui/index.mdx index f0e7cf891..b36d95986 100644 --- a/apps/docs/content/docs/ui/index.mdx +++ b/apps/docs/content/docs/ui/index.mdx @@ -4,6 +4,10 @@ description: Welcome to the VitNode documentation! icon: Power --- + + We're working hard to bring you the best documentation experience. + + Welcome to the docs! You can start writing documents in `/content/docs`. ## What is Next? diff --git a/apps/docs/src/app/global.css b/apps/docs/src/app/global.css index cf465d9a2..a3f1e1bf4 100644 --- a/apps/docs/src/app/global.css +++ b/apps/docs/src/app/global.css @@ -40,9 +40,8 @@ --sidebar-ring: oklch(0.708 0 0); --dev-color: oklch(0.6 0.18 50); - --plugins-color: oklch(0.65 0.18 170); + --ui-color: oklch(0.65 0.18 170); --guides-color: oklch(0.45 0.16 262.61); - --ui-color: oklch(0.5 0.18 280); } .dark { @@ -78,9 +77,8 @@ --sidebar-ring: oklch(0.51 0.16 262.61); --dev-color: oklch(0.75 0.18 50); - --plugins-color: oklch(0.7 0.18 170); + --ui-color: oklch(0.7 0.18 170); --guides-color: oklch(0.65 0.16 262.61); - --ui-color: oklch(0.78 0.18 280); } :root { diff --git a/apps/docs/src/content/docs/guides/sso/discord.mdx b/apps/docs/src/content/docs/guides/sso/discord.mdx index 69fa65f35..edc1e382d 100644 --- a/apps/docs/src/content/docs/guides/sso/discord.mdx +++ b/apps/docs/src/content/docs/guides/sso/discord.mdx @@ -68,7 +68,7 @@ VitNodeAPI({ // [!code ++] authorization: { // [!code ++] - ssoPlugins: [ + ssoProviders: [ // [!code ++] new DiscordSSOApiPlugin({ // [!code ++] diff --git a/apps/docs/src/content/docs/guides/sso/facebook.mdx b/apps/docs/src/content/docs/guides/sso/facebook.mdx index f1d1486f3..8674b2f01 100644 --- a/apps/docs/src/content/docs/guides/sso/facebook.mdx +++ b/apps/docs/src/content/docs/guides/sso/facebook.mdx @@ -62,7 +62,7 @@ VitNodeAPI({ // [!code ++] authorization // [!code ++] - ssoPlugins: [ + ssoProviders: [ // [!code ++] new FacebookSSOApiPlugin({ // [!code ++] diff --git a/apps/docs/src/content/docs/guides/sso/google.mdx b/apps/docs/src/content/docs/guides/sso/google.mdx index d9a9edcf2..c5ce45613 100644 --- a/apps/docs/src/content/docs/guides/sso/google.mdx +++ b/apps/docs/src/content/docs/guides/sso/google.mdx @@ -123,7 +123,7 @@ VitNodeAPI({ // [!code ++] authorization: { // [!code ++] - ssoPlugins: [ + ssoProviders: [ // [!code ++] new GoogleSSOApiPlugin({ // [!code ++] diff --git a/apps/web/src/app/[locale]/(main)/layout.tsx b/apps/web/src/app/[locale]/(main)/layout.tsx index b55a566bb..086bc3368 100644 --- a/apps/web/src/app/[locale]/(main)/layout.tsx +++ b/apps/web/src/app/[locale]/(main)/layout.tsx @@ -1,9 +1,14 @@ import { LogoVitNode } from '@vitnode/core/components/logo-vitnode'; import { ThemeLayout } from '@vitnode/core/views/layouts/theme/layout'; +import { vitNodeConfig } from '../../../vitnode.config'; + export default function Layout({ children }: { children: React.ReactNode }) { return ( - }> + } + vitNodeConfig={vitNodeConfig} + > {children} ); diff --git a/apps/web/src/app/[locale]/layout.tsx b/apps/web/src/app/[locale]/layout.tsx index 7a1615563..f830831ba 100644 --- a/apps/web/src/app/[locale]/layout.tsx +++ b/apps/web/src/app/[locale]/layout.tsx @@ -23,7 +23,7 @@ export const generateMetadata = (): Metadata => generateMetadataRootLayout(vitNodeConfig); export const generateStaticParams = () => - vitNodeConfig.i18n.locales.map(locale => ({ locale })); + vitNodeConfig.i18n.locales.map(locale => ({ locale: locale.code })); export default function LocaleLayout(props: RootLayoutProps) { return ( diff --git a/apps/web/src/locales/@vitnode/blog/pl.json b/apps/web/src/locales/@vitnode/blog/pl.json new file mode 100644 index 000000000..65f92dfff --- /dev/null +++ b/apps/web/src/locales/@vitnode/blog/pl.json @@ -0,0 +1,70 @@ +{ + "@vitnode/blog": { + "title": "Blog", + "admin": { + "nav": { + "posts": "Posts", + "categories": "Categories" + }, + "categories": { + "desc": "Manage categories for blog posts.", + "table": { + "title": "Title", + "updated_at": "Updated At" + }, + "delete": { + "title": "Delete Category", + "desc": "Are you sure you want to delete category? This action cannot be undone.", + "confirm": "Yes, delete this category", + "success": "Category has been deleted successfully." + }, + "create": { + "title": "Create Category", + "desc": "A new category for your blog posts.", + "form": { + "title": { + "label": "Title", + "already_exists": "This category title already exists." + } + }, + "submit": "Create" + }, + "edit": { + "title": "Edit Category", + "submit": "Save Changes" + } + }, + "posts": { + "desc": "Write and manage your blog posts.", + "table": { + "title": "Title", + "category": "Category", + "updated_at": "Updated At" + }, + "create": { + "title": "Create Post", + "desc": "Write a new article for your blog.", + "form": { + "title": { + "label": "Title", + "already_exists": "This post title already exists." + }, + "content": "Content", + "category": "Category" + }, + "submit": "Create Post" + }, + "edit": { + "title": "Edit Post", + "submit": "Save Changes" + }, + "delete": { + "title": "Delete Post", + "desc": "Are you sure you want to delete post? This action cannot be undone.", + "confirm": "Yes, delete this post", + "success": "Post has been deleted successfully." + } + } + } + } +} diff --git a/apps/web/src/locales/@vitnode/core/pl.json b/apps/web/src/locales/@vitnode/core/pl.json new file mode 100644 index 000000000..b89001b73 --- /dev/null +++ b/apps/web/src/locales/@vitnode/core/pl.json @@ -0,0 +1,155 @@ +{ + "core": { + "global": { + "no_results": { + "title": "No results found", + "desc": "Try adjusting your search or filter criteria." + }, + "are_you_sure_want_to_leave_form": { + "title": "Are you sure you want to leave this form?", + "desc": "Your changes will not be saved.", + "cancel": "Cancel", + "confirm": "Yes, leave" + }, + "search_placeholder": "Search...", + "results_not_found": "No results found", + "date": "{date, date}", + "date_medium": "{date, date, medium}", + "date_short": "{date, date, short}", + "register": "Register", + "login": "Login", + "save": "Save", + "submit": "Submit", + "cancel": "Cancel", + "optional": "Optional", + "loading": "Loading...", + "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.", + "field_required": "This field is required.", + "field_min_length": "This field must be at least {min} characters.", + "404": { + "title": "Page Not Found", + "desc": "Oops! The page you're looking for doesn't exist." + }, + "500": { + "title": "Internal Server Error", + "desc": "Sorry, we're experiencing technical difficulties on our server." + }, + "400": { + "title": "Bad Request", + "desc": "The request couldn't be processed due to invalid parameters." + }, + "403": { + "title": "Forbidden", + "desc": "You don't have permission to access this resource." + } + }, + "user_bar": { + "log_out": "Log out", + "admin_cp": "Admin CP" + } + }, + "auth": { + "sso": { + "or": "Or continue With", + "access_denied": "You have denied access to the application or the request has expired. Please try again." + }, + "sign_up": { + "desc": "Hello there! Create your account to get started.", + "already_have_account": "You already have an account? Sign in.", + "submit": "Register", + "username": { + "label": "Username", + "min_length": "Username must be at least 3 characters long.", + "max_length": "Username must be at most 32 characters long.", + "exists": "Username already exists.", + "your_user_code": "Your user code: " + }, + "email": { + "label": "Email", + "invalid": "Invalid email address.", + "exists": "Email already exists." + }, + "password": { + "label": "Password", + "invalid": "Password is too weak.", + "requirements": { + "label": "Password should contain:", + "min_length": "At least 8 characters", + "uppercase": "At least one uppercase letter", + "number": "At least one number", + "special_char": "At least one special character" + } + }, + "terms": { + "label": "Accept terms and conditions", + "required": "You must accept the terms and conditions.", + "desc": "You agree to our Legal documents & Policies." + }, + "newsletter": { + "label": "Newsletter", + "desc": "Receive the latest news and updates." + }, + "email_confirmation": { + "title": "Check your email", + "desc": "We've sent a confirmation link to your email address", + "check_spam": "If you don't see the email in your inbox, please check your spam folder." + } + }, + "sign_in": { + "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": { + "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": "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": { + "desc": "Manage users of your application.", + "user": "User", + "createdAt": "Created At", + "emailNotVerified": "Email Not Verified" + } + } + } +} diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index ac037e605..d3692d532 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -3,7 +3,7 @@ import createMiddleware from 'next-intl/middleware'; import { vitNodeConfig } from './vitnode.config'; export default createMiddleware({ - locales: vitNodeConfig.i18n.locales, + locales: vitNodeConfig.i18n.locales.map(locale => locale.code), defaultLocale: vitNodeConfig.i18n.defaultLocale, localePrefix: vitNodeConfig.i18n.localePrefix, }); diff --git a/apps/web/src/vitnode.api.config.ts b/apps/web/src/vitnode.api.config.ts index 4fd7f2934..bcd8b48e6 100644 --- a/apps/web/src/vitnode.api.config.ts +++ b/apps/web/src/vitnode.api.config.ts @@ -4,15 +4,13 @@ import { DiscordSSOApiPlugin } from '@vitnode/core/api/plugins/sso/discord'; import { FacebookSSOApiPlugin } from '@vitnode/core/api/plugins/sso/facebook'; import { GoogleSSOApiPlugin } from '@vitnode/core/api/plugins/sso/google'; import { buildApiConfig } from '@vitnode/core/vitnode.config'; +import * as dotenv from 'dotenv'; import { drizzle } from 'drizzle-orm/postgres-js'; +import { join } from 'path'; -// import * as dotenv from 'dotenv'; -// import { drizzle } from 'drizzle-orm/postgres-js'; -// import { join } from 'path'; - -// dotenv.config({ -// path: join(process.cwd(), '..', '..', '.env'), -// }); +dotenv.config({ + path: join(process.cwd(), '..', '..', '.env'), +}); export const POSTGRES_URL = // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -31,7 +29,7 @@ export const vitNodeApiConfig = buildApiConfig({ user: process.env.NOD_EMAILER_USER, }), authorization: { - ssoPlugins: [ + ssoProviders: [ DiscordSSOApiPlugin({ clientId: process.env.DISCORD_CLIENT_ID, clientSecret: process.env.DISCORD_CLIENT_SECRET, diff --git a/apps/web/src/vitnode.config.ts b/apps/web/src/vitnode.config.ts index a1fa8f302..2df1ce21d 100644 --- a/apps/web/src/vitnode.config.ts +++ b/apps/web/src/vitnode.config.ts @@ -9,7 +9,16 @@ export const vitNodeConfig = buildConfig({ }, plugins: [blogPlugin()], i18n: { - locales: ['en', 'pl'] as const, + locales: [ + { + code: 'en', + name: 'English (USA)', + }, + { + code: 'pl', + name: 'Polski (PL)', + }, + ], defaultLocale: 'en', }, theme: { diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/middleware.ts b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/middleware.ts index 02d5ae94f..99e9d7051 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/middleware.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/middleware.ts @@ -3,7 +3,7 @@ import createMiddleware from 'next-intl/middleware'; import { vitNodeConfig } from './vitnode.config'; export default createMiddleware({ - locales: vitNodeConfig.i18n.locales, + locales: vitNodeConfig.i18n.locales.map(locale => locale.code), defaultLocale: vitNodeConfig.i18n.defaultLocale, localePrefix: vitNodeConfig.i18n.localePrefix, }); diff --git a/packages/vitnode/scripts/plugin.ts b/packages/vitnode/scripts/plugin.ts index aa4618d60..d9bfc5f8e 100644 --- a/packages/vitnode/scripts/plugin.ts +++ b/packages/vitnode/scripts/plugin.ts @@ -108,6 +108,12 @@ export const processPlugin = ({ initMessage }: { initMessage: string }) => { const cleanupDeletedFiles = (sourceDir: string, destinationDir: string) => { if (!existsSync(destinationDir)) return; + // Check if this is a locale directory - if so, skip cleanup to preserve other language files + const isLocaleDir = destinationDir.includes(join('src', 'locales')); + if (isLocaleDir) { + return; // Skip cleanup for locale directories to preserve files from other plugins/languages + } + const destFiles = getAllFiles(destinationDir); for (const destFile of destFiles) { const relativePath = relative(destinationDir, destFile); diff --git a/packages/vitnode/src/api/middlewares/global/global.ts b/packages/vitnode/src/api/middlewares/global/global.ts index e58556d2e..545206fc9 100644 --- a/packages/vitnode/src/api/middlewares/global/global.ts +++ b/packages/vitnode/src/api/middlewares/global/global.ts @@ -39,7 +39,7 @@ interface EnvVariablesVitNode { cookieSecure: boolean; deviceCookieExpires: number; deviceCookieName: string; - ssoPlugins: SSOApiPlugin[]; + ssoProviders: SSOApiPlugin[]; }; emailProvider?: EmailApiPlugin; metadata: { @@ -88,7 +88,7 @@ export const globalMiddleware = ({ cookieName: authorization?.cookieName ?? 'vitnode_auth', cookie_expires: authorization?.cookieExpires ?? 1000 * 60 * 60 * 24 * 90, // 90 days - ssoPlugins: authorization?.ssoPlugins ?? [], + ssoProviders: authorization?.ssoProviders ?? [], deviceCookieName: authorization?.deviceCookieName ?? 'vitnode_device', deviceCookieExpires: authorization?.deviceCookieExpires ?? 1000 * 60 * 60 * 24 * 365, // 1 year, diff --git a/packages/vitnode/src/api/models/session-admin.ts b/packages/vitnode/src/api/models/session-admin.ts index d3d66e197..4573147f4 100644 --- a/packages/vitnode/src/api/models/session-admin.ts +++ b/packages/vitnode/src/api/models/session-admin.ts @@ -61,8 +61,7 @@ export class SessionAdminModel { setCookie(this.c, this.c.get('core').authorization.adminCookieName, token, { httpOnly: true, secure: this.c.get('core').authorization.cookieSecure, - - path: '/admin', + path: '/', expires: new Date( Date.now() + this.c.get('core').authorization.adminCookieExpires, ), diff --git a/packages/vitnode/src/api/models/sso.ts b/packages/vitnode/src/api/models/sso.ts index 10e13d82f..7cb952ed4 100644 --- a/packages/vitnode/src/api/models/sso.ts +++ b/packages/vitnode/src/api/models/sso.ts @@ -30,7 +30,7 @@ export const getRedirectUri = (code: string) => export class SSOModel { constructor(c: Context) { this.c = c; - this.plugins = c.get('core').authorization.ssoPlugins; + this.plugins = c.get('core').authorization.ssoProviders; } private readonly c: Context; diff --git a/packages/vitnode/src/api/modules/middleware/route.ts b/packages/vitnode/src/api/modules/middleware/route.ts index 7c880d5ed..e8f33690a 100644 --- a/packages/vitnode/src/api/modules/middleware/route.ts +++ b/packages/vitnode/src/api/modules/middleware/route.ts @@ -25,7 +25,7 @@ export const routeMiddleware = buildRoute({ }, }, handler: c => { - const sso = c.get('core').authorization.ssoPlugins; + const sso = c.get('core').authorization.ssoProviders; const email = new EmailModel(c); return c.json({ diff --git a/packages/vitnode/src/components/switchers/langs/language-swietcher.tsx b/packages/vitnode/src/components/switchers/langs/language-swietcher.tsx new file mode 100644 index 000000000..b75c62654 --- /dev/null +++ b/packages/vitnode/src/components/switchers/langs/language-swietcher.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { CheckIcon, LanguagesIcon } from 'lucide-react'; +import { useLocale } from 'next-intl'; +import React from 'react'; + +import type { LocaleConfig } from '@/vitnode.config'; + +import { usePathname, useRouter } from '@/lib/navigation'; + +import { Button } from '../../ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../../ui/dropdown-menu'; + +export const LanguageSwitcher = ({ locales }: { locales: LocaleConfig[] }) => { + const currentLocale = useLocale(); + const [isPending, startTransition] = React.useTransition(); + const { replace } = useRouter(); + + const pathname = usePathname(); + + return ( + + + + + + + {locales.map(locale => ( + { + startTransition(() => { + replace(pathname, { + locale: locale.code, + }); + }); + }} + > + {locale.name} + {locale.code === currentLocale && } + + ))} + + + ); +}; diff --git a/packages/vitnode/src/components/switchers/theme-switcher.tsx b/packages/vitnode/src/components/switchers/themes/theme-switcher.tsx similarity index 93% rename from packages/vitnode/src/components/switchers/theme-switcher.tsx rename to packages/vitnode/src/components/switchers/themes/theme-switcher.tsx index 91cc127b2..eeeb9e05b 100644 --- a/packages/vitnode/src/components/switchers/theme-switcher.tsx +++ b/packages/vitnode/src/components/switchers/themes/theme-switcher.tsx @@ -3,7 +3,7 @@ import { Moon, Sun } from 'lucide-react'; import { useTheme } from 'next-themes'; -import { Button } from '../ui/button'; +import { Button } from '../../ui/button'; export const ThemeSwitcher = () => { const { setTheme, resolvedTheme } = useTheme(); diff --git a/packages/vitnode/src/views/admin/layouts/admin-layout.tsx b/packages/vitnode/src/views/admin/layouts/admin-layout.tsx index 936cb6b03..b8742a6b5 100644 --- a/packages/vitnode/src/views/admin/layouts/admin-layout.tsx +++ b/packages/vitnode/src/views/admin/layouts/admin-layout.tsx @@ -1,7 +1,7 @@ import { getTranslations } from 'next-intl/server'; import { cookies } from 'next/headers'; -import { ThemeSwitcher } from '@/components/switchers/theme-switcher'; +import { ThemeSwitcher } from '@/components/switchers/themes/theme-switcher'; import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; import { SidebarInset } from '@/components/ui/sidebar'; import { getSessionAdminApi } from '@/lib/api/get-session-admin-api'; @@ -10,6 +10,7 @@ import type { VitNodeConfig } from '../../../vitnode.config'; import type { NavAdminParent } from './sidebar/nav/nav'; import { I18nProvider } from '../../../components/i18n-provider'; +import { LanguageSwitcher } from '../../../components/switchers/langs/language-swietcher'; import { SidebarAdmin } from './sidebar/sidebar'; import { UserBarAdmin } from './user-bar/user-bar'; @@ -62,6 +63,7 @@ export const AdminLayout = async ({
+
diff --git a/packages/vitnode/src/views/auth/sso/buttons/sso-buttons.tsx b/packages/vitnode/src/views/auth/sso/buttons/sso-buttons.tsx index 1d22e8757..d5471e8ea 100644 --- a/packages/vitnode/src/views/auth/sso/buttons/sso-buttons.tsx +++ b/packages/vitnode/src/views/auth/sso/buttons/sso-buttons.tsx @@ -20,6 +20,10 @@ export const SSOButtons = async () => { getMiddlewareApi(), ]); + if (!sso.length) { + return null; + } + return ( <>
@@ -27,24 +31,18 @@ export const SSOButtons = async () => {
- {sso.length > 0 && ( -
- - {t('or')} - -
- )} +
+ {t('or')} +
- {sso.length > 0 && ( -
- {sso.map(provider => ( - - {provider.name} - - ))} -
- )} +
+ {sso.map(provider => ( + + {provider.name} + + ))} +
); }; diff --git a/packages/vitnode/src/views/layouts/theme/header/header.tsx b/packages/vitnode/src/views/layouts/theme/header/header.tsx index bad287497..2b4d51a9e 100644 --- a/packages/vitnode/src/views/layouts/theme/header/header.tsx +++ b/packages/vitnode/src/views/layouts/theme/header/header.tsx @@ -1,6 +1,9 @@ -import { Suspense } from 'react'; +import React from 'react'; -import { ThemeSwitcher } from '@/components/switchers/theme-switcher'; +import type { VitNodeConfig } from '@/vitnode.config'; + +import { LanguageSwitcher } from '@/components/switchers/langs/language-swietcher'; +import { ThemeSwitcher } from '@/components/switchers/themes/theme-switcher'; import { Skeleton } from '@/components/ui/skeleton'; import { Link } from '@/lib/navigation'; import { cn } from '@/lib/utils'; @@ -10,8 +13,12 @@ import { UserHeader } from './user/user'; export const HeaderLayout = ({ logo, className, + vitNodeConfig, ...props -}: React.ComponentProps<'header'> & { logo: React.ReactNode }) => { +}: React.ComponentProps<'header'> & { + logo: React.ReactNode; + vitNodeConfig: VitNodeConfig; +}) => { return (
+ - }> + }> - +
diff --git a/packages/vitnode/src/views/layouts/theme/layout.tsx b/packages/vitnode/src/views/layouts/theme/layout.tsx index 7f6c4b6c6..111b7a8e9 100644 --- a/packages/vitnode/src/views/layouts/theme/layout.tsx +++ b/packages/vitnode/src/views/layouts/theme/layout.tsx @@ -1,14 +1,19 @@ +import type { VitNodeConfig } from '../../../vitnode.config'; + import { HeaderLayout } from './header/header'; export const ThemeLayout = ({ children, logo, + vitNodeConfig, }: React.ComponentProps & { children: React.ReactNode; + vitNodeConfig: VitNodeConfig; }) => { return ( <> -
{children}
+ {' '} +
{children}
); }; diff --git a/packages/vitnode/src/vitnode.config.ts b/packages/vitnode/src/vitnode.config.ts index 206ef63be..18ece55a0 100644 --- a/packages/vitnode/src/vitnode.config.ts +++ b/packages/vitnode/src/vitnode.config.ts @@ -6,13 +6,20 @@ import type { EmailApiPlugin } from './api/models/email'; import type { SSOApiPlugin } from './api/models/sso'; import type { BuildPluginReturn } from './lib/plugin'; -export interface VitNodeConfig { +export interface LocaleConfig { + code: string; + name: string; +} + +export interface VitNodeConfig< + AppLocales extends LocaleConfig[] = LocaleConfig[], +> { admin?: { sidebarCookieName?: string; }; debug?: boolean; i18n: { - defaultLocale: AppLocales[number]; + defaultLocale: AppLocales[number]['code']; localePrefix?: 'always' | 'as-needed' | 'never'; locales: AppLocales; timeZone?: string; @@ -37,14 +44,14 @@ export interface VitNodeApiConfig { cookieSecure?: boolean; deviceCookieExpires?: number; deviceCookieName?: string; - ssoPlugins?: SSOApiPlugin[]; + ssoProviders?: SSOApiPlugin[]; }; dbProvider: PostgresJsDatabase; emailProvider?: EmailApiPlugin; plugins: BuildPluginApiReturn[]; } -export function buildConfig( +export function buildConfig( args: VitNodeConfig, ): VitNodeConfig { return { @@ -70,8 +77,9 @@ export const handleRequestConfig = async ({ vitNodeConfig: VitNodeConfig; }) => { const reqLocale = await requestLocale; + const localeCodes = vitNodeConfig.i18n.locales.map(locale => locale.code); const locale = - reqLocale && `${vitNodeConfig.i18n.locales}`.includes(reqLocale) + reqLocale && localeCodes.includes(reqLocale) ? reqLocale : vitNodeConfig.i18n.defaultLocale;